diff --git a/CHANGELOG.md b/CHANGELOG.md index 608ffc4feea..f612bd9e544 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,443 +1,4 @@ -# EmberData Changelog - -## v5.3.4 (2024-06-15) - -#### :evergreen_tree: New Deprecation - -* [#9479](https://github.com/emberjs/data/pull/9479) feat: support migration path for ember-inflector usage ([@runspired](https://github.com/runspired)) -* [#9403](https://github.com/emberjs/data/pull/9403) feat: deprecate store extending EmberObject ([@runspired](https://github.com/runspired)) - -#### :memo: Documentation - -* [#9394](https://github.com/emberjs/data/pull/9394) Add cookbook page about model names convention ([@Baltazore](https://github.com/Baltazore)) -* [#9393](https://github.com/emberjs/data/pull/9393) Update types on typescript guide part 4 ([@Baltazore](https://github.com/Baltazore)) -* [#9390](https://github.com/emberjs/data/pull/9390) docs: fix readme links in @warp-drive/ember ([@runspired](https://github.com/runspired)) -* [#9379](https://github.com/emberjs/data/pull/9379) fix: Automate uninstall process ([@MichalBryxi](https://github.com/MichalBryxi)) -* [#9378](https://github.com/emberjs/data/pull/9378) Update some docs to string ids ([@wagenet](https://github.com/wagenet)) -* [#9300](https://github.com/emberjs/data/pull/9300) doc: remove reference to unexisting ESA auth handler ([@sly7-7](https://github.com/sly7-7)) -* [#9332](https://github.com/emberjs/data/pull/9332) docs: add typescript guide ([@runspired](https://github.com/runspired)) -* [#9328](https://github.com/emberjs/data/pull/9328) chore: update READMEs with status and dist tag info ([@runspired](https://github.com/runspired)) -* [#9329](https://github.com/emberjs/data/pull/9329) chore: update compat chart in README ([@runspired](https://github.com/runspired)) -* [#9299](https://github.com/emberjs/data/pull/9299) doc: use store for save-record docs ([@Yelinz](https://github.com/Yelinz)) -* [#9298](https://github.com/emberjs/data/pull/9298) docs(request): remove duplicate line in readme ([@Yelinz](https://github.com/Yelinz)) -* [#9063](https://github.com/emberjs/data/pull/9063) docs: add requests guide ([@runspired](https://github.com/runspired)) -* [#9215](https://github.com/emberjs/data/pull/9215) Docs: Add guide for incremental adoption ([@Baltazore](https://github.com/Baltazore)) -* [#9275](https://github.com/emberjs/data/pull/9275) doc: don't mention unexisting ESA auth handler ([@sly7-7](https://github.com/sly7-7)) - -#### :rocket: Enhancement - -* [#9474](https://github.com/emberjs/data/pull/9474) Improve query types for legacy-compat/builders ([@gitKrystan](https://github.com/gitKrystan)) -* [#9473](https://github.com/emberjs/data/pull/9473) npx: warp-drive retrofit types@canary 🪄 ([@runspired](https://github.com/runspired)) -* [#9471](https://github.com/emberjs/data/pull/9471) feat: npx warp-drive ([@runspired](https://github.com/runspired)) -* [#9467](https://github.com/emberjs/data/pull/9467) feat: implement schema-object for schema-record ([@richgt](https://github.com/richgt)) -* [#9468](https://github.com/emberjs/data/pull/9468) feat: string utils 🌌 ([@runspired](https://github.com/runspired)) -* [#9466](https://github.com/emberjs/data/pull/9466) feat: make @id editable and reactive ([@runspired](https://github.com/runspired)) -* [#9465](https://github.com/emberjs/data/pull/9465) feat: implement edit & create cases for legacy relationships ([@runspired](https://github.com/runspired)) -* [#9464](https://github.com/emberjs/data/pull/9464) feat: implement support for legacy hasMany and belongsTo relationship reads ([@runspired](https://github.com/runspired)) -* [#9407](https://github.com/emberjs/data/pull/9407) feat: v2 addons ([@runspired](https://github.com/runspired)) -* [#9453](https://github.com/emberjs/data/pull/9453) feat: update SchemaService to reflect RFC updates ([@runspired](https://github.com/runspired)) -* [#9448](https://github.com/emberjs/data/pull/9448) feat: impl SchemaService RFC ([@runspired](https://github.com/runspired)) -* [#9450](https://github.com/emberjs/data/pull/9450) feat: improve typing around Model and createRecord ([@runspired](https://github.com/runspired)) -* [#9444](https://github.com/emberjs/data/pull/9444) feat: rename LifetimesService => CachePolicy for clarity ([@runspired](https://github.com/runspired)) -* [#9443](https://github.com/emberjs/data/pull/9443) feat: universal consts ([@runspired](https://github.com/runspired)) -* [#9396](https://github.com/emberjs/data/pull/9396) fix: Resolve promise types for props passed to `store.createRecord()` ([@seanCodes](https://github.com/seanCodes)) -* [#9401](https://github.com/emberjs/data/pull/9401) feat: preserve lids returned by the API in legacy normalization ([@runspired](https://github.com/runspired)) -* [#9400](https://github.com/emberjs/data/pull/9400) feat: add expectId util ([@runspired](https://github.com/runspired)) -* [#9343](https://github.com/emberjs/data/pull/9343) @ember-data/codemods package ([@gitKrystan](https://github.com/gitKrystan)) -* [#9387](https://github.com/emberjs/data/pull/9387) feat: better types for legacy store methods ([@runspired](https://github.com/runspired)) -* [#8957](https://github.com/emberjs/data/pull/8957) feat(private): schema CLI ([@runspired](https://github.com/runspired)) -* [#9366](https://github.com/emberjs/data/pull/9366) feat: typed Model ([@runspired](https://github.com/runspired)) -* [#9363](https://github.com/emberjs/data/pull/9363) feat: autoRefresh ([@runspired](https://github.com/runspired)) -* [#9359](https://github.com/emberjs/data/pull/9359) feat: type checked builders and inferred request types from builders ([@runspired](https://github.com/runspired)) -* [#9353](https://github.com/emberjs/data/pull/9353) feat: utilies for migrating to stricter type and id usage ([@runspired](https://github.com/runspired)) -* [#9352](https://github.com/emberjs/data/pull/9352) feat: make setKeyInfoForResource public ([@runspired](https://github.com/runspired)) -* [#9277](https://github.com/emberjs/data/pull/9277) feat: implement managed object for schemaRecord ([@richgt](https://github.com/richgt)) -* [#9319](https://github.com/emberjs/data/pull/9319) Add @ember-data/legacy-compat/builders ([@gitKrystan](https://github.com/gitKrystan)) -* [#9314](https://github.com/emberjs/data/pull/9314) feat: improve lifetime handling of ad-hoc createRecord requests ([@runspired](https://github.com/runspired)) -* [#9317](https://github.com/emberjs/data/pull/9317) feat: ensure data utils work well with legacy relationship proxies ([@runspired](https://github.com/runspired)) -* [#9260](https://github.com/emberjs/data/pull/9260) feat: ember specific data utils ([@runspired](https://github.com/runspired)) -* [#9240](https://github.com/emberjs/data/pull/9240) feat: implement managed array for schemaRecord ([@richgt](https://github.com/richgt)) -* [#9256](https://github.com/emberjs/data/pull/9256) feat: improve alpha types support ([@runspired](https://github.com/runspired)) -* [#9250](https://github.com/emberjs/data/pull/9250) feat: fix types for legacy decorator syntax ([@runspired](https://github.com/runspired)) -* [#9249](https://github.com/emberjs/data/pull/9249) chore: handle declare statements in module rewriting ([@runspired](https://github.com/runspired)) -* [#9248](https://github.com/emberjs/data/pull/9248) feat: publish types as module defs ([@runspired](https://github.com/runspired)) -* [#9245](https://github.com/emberjs/data/pull/9245) feat: add consumer types for Model APIs ([@runspired](https://github.com/runspired)) -* [#9246](https://github.com/emberjs/data/pull/9246) normalization in json-api serializer preserves lid #7956 ([@sly7-7](https://github.com/sly7-7)) -* [#9244](https://github.com/emberjs/data/pull/9244) feat: improves consumer-facing store types ([@runspired](https://github.com/runspired)) - -#### :bug: Bug Fix - -* [#9475](https://github.com/emberjs/data/pull/9475) fix: dont install optional peers if not already present ([@runspired](https://github.com/runspired)) -* [#9469](https://github.com/emberjs/data/pull/9469) Fix exports for 'ember-data' ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) -* [#9459](https://github.com/emberjs/data/pull/9459) fix: ensure cachehandler responses are cast to documents ([@runspired](https://github.com/runspired)) -* [#9456](https://github.com/emberjs/data/pull/9456) fix: visibilitychange => hidden should update unavailableStart ([@runspired](https://github.com/runspired)) -* [#9454](https://github.com/emberjs/data/pull/9454) Allow RequestState.abort to be used with on modifier ([@gitKrystan](https://github.com/gitKrystan)) -* [#9455](https://github.com/emberjs/data/pull/9455) fix: config version lookup needs to be project location aware ([@runspired](https://github.com/runspired)) -* [#9355](https://github.com/emberjs/data/pull/9355) Fix: @attr defaultValue() results should persist after initialization ([@christophersansone](https://github.com/christophersansone)) -* [#9391](https://github.com/emberjs/data/pull/9391) fix: dont fall-through after shouldAttempt on refresh ([@runspired](https://github.com/runspired)) -* [#9383](https://github.com/emberjs/data/pull/9383) fix: ensure cache-handler clones full errors ([@runspired](https://github.com/runspired)) -* [#9369](https://github.com/emberjs/data/pull/9369) fix: @warp-drive-ember, dont leak empty slot ([@runspired](https://github.com/runspired)) -* [#9364](https://github.com/emberjs/data/pull/9364) fix: restore old behavior in deprecation ([@enspandi](https://github.com/enspandi)) -* [#9360](https://github.com/emberjs/data/pull/9360) fix: Make IS_MAYBE_MIRAGE work in Firefox ([@MichalBryxi](https://github.com/MichalBryxi)) -* [#9318](https://github.com/emberjs/data/pull/9318) fix: be more specific in files in case .npmignore is ignored ([@runspired](https://github.com/runspired)) -* [#9307](https://github.com/emberjs/data/pull/9307) fix: mirage does not support anything ([@runspired](https://github.com/runspired)) -* [#9265](https://github.com/emberjs/data/pull/9265) feat: Improve config handling for polyfillUUID ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) -* [#9263](https://github.com/emberjs/data/pull/9263) fix: set localState to latest identifier in belongsTo when merging identifiers ([@runspired](https://github.com/runspired)) -* [#9254](https://github.com/emberjs/data/pull/9254) Update IS_MAYBE_MIRAGE function to check for Mirage in development mode ([@Baltazore](https://github.com/Baltazore)) -* [#9257](https://github.com/emberjs/data/pull/9257) fix: use npm pack instead of pnpm pack to respect .npmignore rules ([@runspired](https://github.com/runspired)) -* [#9252](https://github.com/emberjs/data/pull/9252) fix: update line when removing declare statements ([@runspired](https://github.com/runspired)) -* [#9251](https://github.com/emberjs/data/pull/9251) fix: notify during replace if existing localState never previously calculated ([@runspired](https://github.com/runspired)) - -#### :house: Internal - -* [#9477](https://github.com/emberjs/data/pull/9477) fix: add deprecation and avoid breaking configs ([@runspired](https://github.com/runspired)) -* [#9476](https://github.com/emberjs/data/pull/9476) chore: cleanup symbol usage ([@runspired](https://github.com/runspired)) -* [#9463](https://github.com/emberjs/data/pull/9463) types: ManyArray => HasMany ([@runspired](https://github.com/runspired)) -* [#9457](https://github.com/emberjs/data/pull/9457) feat: the big list of versions ([@runspired](https://github.com/runspired)) -* [#9292](https://github.com/emberjs/data/pull/9292) feat: add new build-config package ([@runspired](https://github.com/runspired)) -* [#9399](https://github.com/emberjs/data/pull/9399) types: limit traversal depth on include path generation ([@runspired](https://github.com/runspired)) -* [#9398](https://github.com/emberjs/data/pull/9398) chore: dont --compile during prepack ([@runspired](https://github.com/runspired)) -* [#9397](https://github.com/emberjs/data/pull/9397) chore: fixup publish for @ember-data/codemods ([@runspired](https://github.com/runspired)) -* [#9395](https://github.com/emberjs/data/pull/9395) Update strategy.json to mirror publish @warp-drive/schema-record ([@runspired](https://github.com/runspired)) -* [#9385](https://github.com/emberjs/data/pull/9385) fix: Make IS_MAYBE_MIRAGE simplified ([@MichalBryxi](https://github.com/MichalBryxi)) -* [#9392](https://github.com/emberjs/data/pull/9392) Fix some typos after reading code ([@Baltazore](https://github.com/Baltazore)) -* [#9370](https://github.com/emberjs/data/pull/9370) chore: rename macros ([@runspired](https://github.com/runspired)) -* [#9368](https://github.com/emberjs/data/pull/9368) docs: Update ISSUE_TEMPLATE.md to follow latest pnpm ([@MichalBryxi](https://github.com/MichalBryxi)) -* [#9365](https://github.com/emberjs/data/pull/9365) chore: remove unneeded infra tests ([@runspired](https://github.com/runspired)) -* [#9349](https://github.com/emberjs/data/pull/9349) chore: fix CI installs ([@runspired](https://github.com/runspired)) -* [#9330](https://github.com/emberjs/data/pull/9330) chore: ensure latest tag is canary/beta tag for early stage packages ([@runspired](https://github.com/runspired)) -* [#9303](https://github.com/emberjs/data/pull/9303) infra: setup mirror and types publishing ([@runspired](https://github.com/runspired)) -* [#9291](https://github.com/emberjs/data/pull/9291) chore: remove unused scripts ([@runspired](https://github.com/runspired)) -* [#9289](https://github.com/emberjs/data/pull/9289) chore: bump timeout for floating dep check in CI ([@runspired](https://github.com/runspired)) -* [#9287](https://github.com/emberjs/data/pull/9287) chore: bump deps for example-api app ([@runspired](https://github.com/runspired)) -* [#9279](https://github.com/emberjs/data/pull/9279) types: branded transforms and improve types needed for serializers ([@runspired](https://github.com/runspired)) -* [#9280](https://github.com/emberjs/data/pull/9280) chore: handle dynamic imports with relative paths ([@runspired](https://github.com/runspired)) -* [#9259](https://github.com/emberjs/data/pull/9259) Update setting-up-the-project.md ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) -* [#9258](https://github.com/emberjs/data/pull/9258) fix: remove unused turbo key ([@runspired](https://github.com/runspired)) - -#### Committers: (13) - -Chris Thoburn ([@runspired](https://github.com/runspired)) -Kirill Shaplyko ([@Baltazore](https://github.com/Baltazore)) -Michal Bryxí ([@MichalBryxi](https://github.com/MichalBryxi)) -Peter Wagenet ([@wagenet](https://github.com/wagenet)) -Sylvain Mina ([@sly7-7](https://github.com/sly7-7)) -Yelin Zhang ([@Yelinz](https://github.com/Yelinz)) -Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) -Rich Glazerman ([@richgt](https://github.com/richgt)) -Sean Juarez ([@seanCodes](https://github.com/seanCodes)) -[@NullVoxPopuli](https://github.com/NullVoxPopuli) -Christopher Sansone ([@christophersansone](https://github.com/christophersansone)) -Andreas Minnich ([@enspandi](https://github.com/enspandi)) -Mehul Kiran Chaudhari ([@MehulKChaudhari](https://github.com/MehulKChaudhari)) - -## v5.3.3 (2024-03-02) - -#### :bug: Bug Fix - -* [#9243](https://github.com/emberjs/data/pull/9243) fix: keep core-type peer-deps ([@runspired](https://github.com/runspired)) - -#### Committers: (1) - -Chris Thoburn ([@runspired](https://github.com/runspired)) - -## v5.3.2 (2024-02-29) - -#### :house: Internal - -* [#9241](https://github.com/emberjs/data/pull/9241) chore: manually run prepack ([@runspired](https://github.com/runspired)) -* [#9238](https://github.com/emberjs/data/pull/9238) chore: better "from" version default value population ([@runspired](https://github.com/runspired)) -* [#9237](https://github.com/emberjs/data/pull/9237) chore: fix publishing of core-types when strategy is private ([@runspired](https://github.com/runspired)) - -#### Committers: (1) - -Chris Thoburn ([@runspired](https://github.com/runspired)) - -## v5.3.1 (2024-02-24) - -#### :evergreen_tree: New Deprecation - -* [#9189](https://github.com/emberjs/data/pull/9189) fix: mutating ManyArray should handle duplicates gracefully (with deprecation) ([@gitKrystan](https://github.com/gitKrystan)) - -#### :memo: Documentation - -* [#9132](https://github.com/emberjs/data/pull/9132) Add auth handler guides ([@Baltazore](https://github.com/Baltazore)) -* [#9071](https://github.com/emberjs/data/pull/9071) chore: refactor relationships guide ([@runspired](https://github.com/runspired)) -* [#9059](https://github.com/emberjs/data/pull/9059) docs: The comprehensive guide to relationships ([@runspired](https://github.com/runspired)) -* [#9018](https://github.com/emberjs/data/pull/9018) doc(README): remove typo ([@omimakhare](https://github.com/omimakhare)) -* [#8966](https://github.com/emberjs/data/pull/8966) feat: Add links to the CODE_OF_CONDUCT.md ([@Agnik7](https://github.com/Agnik7)) -* [#8963](https://github.com/emberjs/data/pull/8963) chore: scaffold additional contributing materials ([@runspired](https://github.com/runspired)) -* [#9162](https://github.com/emberjs/data/pull/9162) feat: improve store.request documentation ([@runspired](https://github.com/runspired)) -* [#9161](https://github.com/emberjs/data/pull/9161) docs: fix return signature of peekRequest ([@runspired](https://github.com/runspired)) -* [#9159](https://github.com/emberjs/data/pull/9159) fix: support full range of json:api for references, update docs ([@runspired](https://github.com/runspired)) -* [#9160](https://github.com/emberjs/data/pull/9160) docs: update links ([@runspired](https://github.com/runspired)) -* [#8954](https://github.com/emberjs/data/pull/8954) docs: typo in hasChangedRelationships description ([@BoussonKarel](https://github.com/BoussonKarel)) -* [#9072](https://github.com/emberjs/data/pull/9072) feat: advanced JSON:API queries & basic request example ([@runspired](https://github.com/runspired)) -* [#9070](https://github.com/emberjs/data/pull/9070) docs: fix note notation to make use of github formatting ([@runspired](https://github.com/runspired)) -* [#9068](https://github.com/emberjs/data/pull/9068) docs: unroll details sections ([@runspired](https://github.com/runspired)) - -#### :rocket: Enhancement - -* [#9220](https://github.com/emberjs/data/pull/9220) feat: request infra improvements ([@runspired](https://github.com/runspired)) -* [#9163](https://github.com/emberjs/data/pull/9163) feat: improved lifetimes-service capabilities ([@runspired](https://github.com/runspired)) -* [#9159](https://github.com/emberjs/data/pull/9159) fix: support full range of json:api for references, update docs ([@runspired](https://github.com/runspired)) -* [#9094](https://github.com/emberjs/data/pull/9094) feat: support legacy attribute behaviors in SchemaRecord ([@gitKrystan](https://github.com/gitKrystan)) -* [#9095](https://github.com/emberjs/data/pull/9095) feat (internal): support legacy model behaviors in SchemaRecord legacy mode ([@runspired](https://github.com/runspired)) -* [#9072](https://github.com/emberjs/data/pull/9072) feat: advanced JSON:API queries & basic request example ([@runspired](https://github.com/runspired)) -* [#9069](https://github.com/emberjs/data/pull/9069) feat: Improve extensibility ([@runspired](https://github.com/runspired)) -* [#8955](https://github.com/emberjs/data/pull/8955) feat(private): scaffold packages for schema parser ([@runspired](https://github.com/runspired)) -* [#8949](https://github.com/emberjs/data/pull/8949) feat:prepare for universal reactivity ([@runspired](https://github.com/runspired)) -* [#8948](https://github.com/emberjs/data/pull/8948) feat(private): reactive simple fields ([@runspired](https://github.com/runspired)) -* [#8946](https://github.com/emberjs/data/pull/8946) feat (private): implement resource relationships for SchemaRecord ([@runspired](https://github.com/runspired)) -* [#8939](https://github.com/emberjs/data/pull/8939) feat (private): implement support for derivations in schema-record ([@runspired](https://github.com/runspired)) -* [#8935](https://github.com/emberjs/data/pull/8935) feat: (private) implement basic field support for schema-record ([@runspired](https://github.com/runspired)) -* [#8925](https://github.com/emberjs/data/pull/8925) feat: implement postQuery builder ([@runspired](https://github.com/runspired)) -* [#8921](https://github.com/emberjs/data/pull/8921) feat: Improved Fetch Errors ([@runspired](https://github.com/runspired)) - -#### :bug: Bug Fix - -* [#9221](https://github.com/emberjs/data/pull/9221) fix: prevent rollbackRelationships from setting remoteState and localState to the same array reference ([@runspired](https://github.com/runspired)) -* [#9203](https://github.com/emberjs/data/pull/9203) fix: Fetch handler hacks for Mirage (canary) ([@gitKrystan](https://github.com/gitKrystan)) -* [#9189](https://github.com/emberjs/data/pull/9189) fix: mutating ManyArray should handle duplicates gracefully (with deprecation) ([@gitKrystan](https://github.com/gitKrystan)) -* [#9183](https://github.com/emberjs/data/pull/9183) fix: keep a backreference for previously merged identifiers ([@runspired](https://github.com/runspired)) -* [#8927](https://github.com/emberjs/data/pull/8927) fix: live-array delete sync should not clear the set on length match ([@runspired](https://github.com/runspired)) -* [#9164](https://github.com/emberjs/data/pull/9164) fix: url configuration should respect / for host and error more meaningfully when invalid ([@runspired](https://github.com/runspired)) -* [#9159](https://github.com/emberjs/data/pull/9159) fix: support full range of json:api for references, update docs ([@runspired](https://github.com/runspired)) -* [#9097](https://github.com/emberjs/data/pull/9097) fix: allow decorator syntax in code comments during yui doc processing ([@jaredgalanis](https://github.com/jaredgalanis)) -* [#9014](https://github.com/emberjs/data/pull/9014) fix: make willCommit slightly safer when race conditions occur ([@runspired](https://github.com/runspired)) -* [#8934](https://github.com/emberjs/data/pull/8934) fix: JSONAPISerializer should not reify empty records ([@runspired](https://github.com/runspired)) -* [#8892](https://github.com/emberjs/data/pull/8892) doc: Fix paths in transform deprecations ([@HeroicEric](https://github.com/HeroicEric)) - -#### :house: Internal - -* [#9125](https://github.com/emberjs/data/pull/9125) Configure ESLint for test packages ([@gitKrystan](https://github.com/gitKrystan)) -* [#8994](https://github.com/emberjs/data/pull/8994) chore: fix recursive pnpm on node 18.18 ([@runspired](https://github.com/runspired)) -* [#9110](https://github.com/emberjs/data/pull/9110) Stricter typescript-eslint config ([@gitKrystan](https://github.com/gitKrystan)) -* [#9101](https://github.com/emberjs/data/pull/9101) chore: Type check test files ([@gitKrystan](https://github.com/gitKrystan)) -* [#9093](https://github.com/emberjs/data/pull/9093) feat(internal): implement legacy mode toggle ([@runspired](https://github.com/runspired)) -* [#9085](https://github.com/emberjs/data/pull/9085) Add type-checking to tests/warp-drive__schema-record ([@gitKrystan](https://github.com/gitKrystan)) -* [#9089](https://github.com/emberjs/data/pull/9089) Add type-checking for packages/unpublished-test-infra ([@gitKrystan](https://github.com/gitKrystan)) -* [#9009](https://github.com/emberjs/data/pull/9009) chore(internal) add @warp-drive/diagnostic/ember ([@runspired](https://github.com/runspired)) -* [#9007](https://github.com/emberjs/data/pull/9007) chore(internal): convert model and adapter tests to use diagnostic ([@runspired](https://github.com/runspired)) -* [#8967](https://github.com/emberjs/data/pull/8967) chore(private): implements a QUnit alternative ([@runspired](https://github.com/runspired)) -* [#9086](https://github.com/emberjs/data/pull/9086) Add ESLint config for tests/warp-drive__schema-record ([@gitKrystan](https://github.com/gitKrystan)) -* [#9078](https://github.com/emberjs/data/pull/9078) docs: add compatibility table to readme ([@runspired](https://github.com/runspired)) -* [#9054](https://github.com/emberjs/data/pull/9054) Initial lint config for tests/blueprints ([@gitKrystan](https://github.com/gitKrystan)) -* [#9061](https://github.com/emberjs/data/pull/9061) Git-ignore .prettier-cache ([@gitKrystan](https://github.com/gitKrystan)) -* [#8993](https://github.com/emberjs/data/pull/8993) chore: fix development test command ([@runspired](https://github.com/runspired)) -* [#8986](https://github.com/emberjs/data/pull/8986) chore: rename schema tests to warp-drive__* variants ([@runspired](https://github.com/runspired)) -* [#8984](https://github.com/emberjs/data/pull/8984) chore: remove unneeded debug-encapsulation tests ([@runspired](https://github.com/runspired)) -* [#8983](https://github.com/emberjs/data/pull/8983) chore: rename request-test-app to ember-data__request ([@runspired](https://github.com/runspired)) -* [#8982](https://github.com/emberjs/data/pull/8982) chore: rename json-api-test-app to ember-data__json-api ([@runspired](https://github.com/runspired)) -* [#8981](https://github.com/emberjs/data/pull/8981) chore: rename adapter-encapsulation-test-app to ember-data__adapter ([@runspired](https://github.com/runspired)) -* [#8980](https://github.com/emberjs/data/pull/8980) chore: rename graph-test-app to ember-data__graph ([@runspired](https://github.com/runspired)) -* [#8979](https://github.com/emberjs/data/pull/8979) chore: rename serializer-encapsulation tests, remove smoke-test ([@runspired](https://github.com/runspired)) -* [#8978](https://github.com/emberjs/data/pull/8978) chore: rename model-encapsulation tests, remove smoke-test ([@runspired](https://github.com/runspired)) -* [#8974](https://github.com/emberjs/data/pull/8974) chore: remove uneeded json-api-encapsulation test app ([@runspired](https://github.com/runspired)) -* [#8960](https://github.com/emberjs/data/pull/8960) internal: fix test settledness ([@runspired](https://github.com/runspired)) -* [#9084](https://github.com/emberjs/data/pull/9084) Add import types ([@gitKrystan](https://github.com/gitKrystan)) -* [#8989](https://github.com/emberjs/data/pull/8989) chore(private): concurrent mode ([@runspired](https://github.com/runspired)) -* [#9082](https://github.com/emberjs/data/pull/9082) Remove remaining @types/ember* packages ([@gitKrystan](https://github.com/gitKrystan)) -* [#8961](https://github.com/emberjs/data/pull/8961) chore: run tests nicely ([@runspired](https://github.com/runspired)) -* [#9062](https://github.com/emberjs/data/pull/9062) Extract qunit ESLint config ([@gitKrystan](https://github.com/gitKrystan)) -* [#9058](https://github.com/emberjs/data/pull/9058) Switch from eslint-plugin-prettier to running prettier directly ([@gitKrystan](https://github.com/gitKrystan)) -* [#9057](https://github.com/emberjs/data/pull/9057) Add eslint-plugin-n to eslint config for node files ([@gitKrystan](https://github.com/gitKrystan)) -* [#9055](https://github.com/emberjs/data/pull/9055) Fix ESLint for VSCode ([@gitKrystan](https://github.com/gitKrystan)) -* [#9051](https://github.com/emberjs/data/pull/9051) chore: use references for tsc, add checks to schema-record, bun to run scripts ([@runspired](https://github.com/runspired)) -* [#9032](https://github.com/emberjs/data/pull/9032) chore(types): split out lint and type commands to be per-package ([@runspired](https://github.com/runspired)) -* [#9050](https://github.com/emberjs/data/pull/9050) chore: use composite mode for tsc ([@runspired](https://github.com/runspired)) -* [#9049](https://github.com/emberjs/data/pull/9049) chore: incremental tsc builds ([@runspired](https://github.com/runspired)) -* [#9046](https://github.com/emberjs/data/pull/9046) chore: reduce number of things turbo builds for build ([@runspired](https://github.com/runspired)) -* [#9027](https://github.com/emberjs/data/pull/9027) chore: improve types for store package ([@runspired](https://github.com/runspired)) -* [#9029](https://github.com/emberjs/data/pull/9029) chore: add @warp-drive/core as home for shared code ([@runspired](https://github.com/runspired)) -* [#9028](https://github.com/emberjs/data/pull/9028) chore: more isolated types ([@runspired](https://github.com/runspired)) -* [#9025](https://github.com/emberjs/data/pull/9025) chore: reconfigure request package type location ([@runspired](https://github.com/runspired)) -* [#9024](https://github.com/emberjs/data/pull/9024) chore: cleanup more types ([@runspired](https://github.com/runspired)) -* [#9021](https://github.com/emberjs/data/pull/9021) chore: cleanup ember-data/-private types ([@runspired](https://github.com/runspired)) -* [#9019](https://github.com/emberjs/data/pull/9019) chore: make model types strict ([@runspired](https://github.com/runspired)) -* [#9017](https://github.com/emberjs/data/pull/9017) chore: make json-api cache strict ([@runspired](https://github.com/runspired)) -* [#9016](https://github.com/emberjs/data/pull/9016) chore: make type-only files strict ([@runspired](https://github.com/runspired)) -* [#9008](https://github.com/emberjs/data/pull/9008) chore: update eslint plugin name ([@runspired](https://github.com/runspired)) -* [#9006](https://github.com/emberjs/data/pull/9006) chore (internal): convert builder and request tests to use diagnostic+runner ([@runspired](https://github.com/runspired)) -* [#9000](https://github.com/emberjs/data/pull/9000) feat(private): native test runner ([@runspired](https://github.com/runspired)) -* [#8995](https://github.com/emberjs/data/pull/8995) chore: add @warp-drive/diagnostic docs ([@runspired](https://github.com/runspired)) -* [#8987](https://github.com/emberjs/data/pull/8987) chore: test-harness improvements ([@runspired](https://github.com/runspired)) -* [#8972](https://github.com/emberjs/data/pull/8972) chore: use new test runner for request tests ([@runspired](https://github.com/runspired)) -* [#8931](https://github.com/emberjs/data/pull/8931) chore: package infra for schema-record ([@runspired](https://github.com/runspired)) -* [#8930](https://github.com/emberjs/data/pull/8930) chore: get last request for any record on instantiation ([@runspired](https://github.com/runspired)) -* [#8923](https://github.com/emberjs/data/pull/8923) chore: prepare files for new eslint plugin ([@runspired](https://github.com/runspired)) -* [#8911](https://github.com/emberjs/data/pull/8911) chore: remove unneeded type cast ([@runspired](https://github.com/runspired)) -* [#8912](https://github.com/emberjs/data/pull/8912) chore: docs for holodeck ([@runspired](https://github.com/runspired)) -* [#8906](https://github.com/emberjs/data/pull/8906) feat: expand mock-server capabilities, add to main tests ([@runspired](https://github.com/runspired)) - -#### Committers: (8) - -Krystan HuffMenne ([@gitKrystan](https://github.com/gitKrystan)) -Kirill Shaplyko ([@Baltazore](https://github.com/Baltazore)) -Chris Thoburn ([@runspired](https://github.com/runspired)) -OMKAR MAKHARE ([@omimakhare](https://github.com/omimakhare)) -Agnik Bakshi ([@Agnik7](https://github.com/Agnik7)) -[@BoussonKarel](https://github.com/BoussonKarel) -Jared Galanis ([@jaredgalanis](https://github.com/jaredgalanis)) -Eric Kelly ([@HeroicEric](https://github.com/HeroicEric)) - -## v5.3.0 (2023-09-18) - -#### :rocket: Enhancement - * [#8849](https://github.com/emberjs/data/pull/8849) feat: docs, tests and fixes for create/update/deleteRecord builders ([@Baltazore](https://github.com/Baltazore)) - * [#8824](https://github.com/emberjs/data/pull/8824) feat: relationshipRollback, serializePatch ([@runspired](https://github.com/runspired)) - * [#8798](https://github.com/emberjs/data/pull/8798) feat: implement a simple LifetimeService utility, improve document reconstruction ([@runspired](https://github.com/runspired)) - * [#8741](https://github.com/emberjs/data/pull/8741) feat: JSON:API serialization utils ([@runspired](https://github.com/runspired)) - * [#8740](https://github.com/emberjs/data/pull/8740) feat: saveRecord builders ([@runspired](https://github.com/runspired)) - * [#8744](https://github.com/emberjs/data/pull/8744) add sortQueryParams, update roadmap with link to checklist ([@runspired](https://github.com/runspired)) - * [#8716](https://github.com/emberjs/data/pull/8716) feat: filterEmpty for query params ([@runspired](https://github.com/runspired)) - * [#8687](https://github.com/emberjs/data/pull/8687) feat: findRecord and query request builders ([@runspired](https://github.com/runspired)) - * [#8673](https://github.com/emberjs/data/pull/8673) DX: Nicer backtracking errors ([@runspired](https://github.com/runspired)) - * [#8736](https://github.com/emberjs/data/pull/8736) chore: refactor IdentityCache to make resource more opaque ([@runspired](https://github.com/runspired)) - -#### :bug: Bug Fix - * [#8876](https://github.com/emberjs/data/pull/8876) fix: Fetch handler should account for empty body ([@runspired](https://github.com/runspired)) - * [#8842](https://github.com/emberjs/data/pull/8842) fix: handle Immutable Response objects ([@runspired](https://github.com/runspired)) - * [#8828](https://github.com/emberjs/data/pull/8828) fix: set headers after setResponse in Fetch handler ([@runspired](https://github.com/runspired)) - * [#8850](https://github.com/emberjs/data/pull/8850) Overwrite addMixin ([@patricklx](https://github.com/patricklx)) - * [#8831](https://github.com/emberjs/data/pull/8831) fix: cleanup build deps and add @ember/string to REST/ActiveRecord builder peer-deps ([@runspired](https://github.com/runspired)) - * [#8826](https://github.com/emberjs/data/pull/8826) fix createRecord error when no adapter is present ([@runspired](https://github.com/runspired)) - * [#8791](https://github.com/emberjs/data/pull/8791) fix: clear relationships properly when unloading new records ([@Windvis](https://github.com/Windvis)) - * [#8794](https://github.com/emberjs/data/pull/8794) Fix check for new records in JSONAPISerializer.serializeHasMany ([@dagroe](https://github.com/dagroe)) - * [#8751](https://github.com/emberjs/data/pull/8751) Forward fixes from 3.12.x into main ([@jrjohnson](https://github.com/jrjohnson)) - * [#8684](https://github.com/emberjs/data/pull/8684) fix: unloadAll(void) should not destroy the notification manager ([@runspired](https://github.com/runspired)) - -#### :evergreen_tree: New Deprecation - * [#8747](https://github.com/emberjs/data/pull/8747) feat: implement legacy imports deprecation ([@runspired](https://github.com/runspired)) - * [#8734](https://github.com/emberjs/data/pull/8734) feat: Implement Strict Types and Id Deprecations ([@runspired](https://github.com/runspired)) - -#### :shower: Deprecation Removal -* `adapter`, `model`, `private-build-infra`, `serializer` - * [#8797](https://github.com/emberjs/data/pull/8797) Drop support for `ember-cli-mocha` and `ember-mocha` when generating test blueprints ([@bertdeblock](https://github.com/bertdeblock)) - -#### :memo: Documentation - * [#8848](https://github.com/emberjs/data/pull/8848) feat: add request options documentation parts to find-record builder ([@Baltazore](https://github.com/Baltazore)) - * [#8825](https://github.com/emberjs/data/pull/8825) feat: more docs for builders ([@runspired](https://github.com/runspired)) - * [#8819](https://github.com/emberjs/data/pull/8819) fix: `JSONAPISerializer.shouldSerializeHasMany` relation param type ([@samridhivig](https://github.com/samridhivig)) - * [#8746](https://github.com/emberjs/data/pull/8746) docs: more documentation for builders ([@runspired](https://github.com/runspired)) - * [#8745](https://github.com/emberjs/data/pull/8745) chore: readme overviews for builders ([@runspired](https://github.com/runspired)) - * [#8724](https://github.com/emberjs/data/pull/8724) chore: rename CacheStoreWrapper => CacheCapabilitiesManager to reflect its role ([@runspired](https://github.com/runspired)) - * [#8671](https://github.com/emberjs/data/pull/8671) Typo correction in ROADMAP.md ([@wagenet](https://github.com/wagenet)) - -#### :goal_net: Test - * [#8878](https://github.com/emberjs/data/pull/8878) test: add basic test for Fetch handler ([@runspired](https://github.com/runspired)) - * [#8849](https://github.com/emberjs/data/pull/8849) feat: docs, tests and fixes for create/update/deleteRecord builders ([@Baltazore](https://github.com/Baltazore)) - * [#8868](https://github.com/emberjs/data/pull/8868) Add tests for filter-empty request util ([@Baltazore](https://github.com/Baltazore)) - * [#8866](https://github.com/emberjs/data/pull/8866) Add tests for parse-cache-control ([@Baltazore](https://github.com/Baltazore)) - * [#8864](https://github.com/emberjs/data/pull/8864) test: confirm records unload properly for #8863 ([@runspired](https://github.com/runspired)) - * [#8780](https://github.com/emberjs/data/pull/8780) chore: add test to demonstrate create props work as expected ([@runspired](https://github.com/runspired)) - -#### :house: Internal - * [#8758](https://github.com/emberjs/data/pull/8758) chore: refactor implicit edge to match resource and collection pattern ([@runspired](https://github.com/runspired)) - * [#8755](https://github.com/emberjs/data/pull/8755) chore: simplify file structure in graph package ([@runspired](https://github.com/runspired)) - * [#8749](https://github.com/emberjs/data/pull/8749) fix: ensure we are not allowing embroider to do anything ([@runspired](https://github.com/runspired)) - * [#8672](https://github.com/emberjs/data/pull/8672) chore: update roadmap for 5.3 ([@runspired](https://github.com/runspired)) - * [#8670](https://github.com/emberjs/data/pull/8670) chore: add ROADMAP and update CONTRIBUTING ([@runspired](https://github.com/runspired)) - * [#8739](https://github.com/emberjs/data/pull/8739) chore: migrate store/graph to strict types config ([@runspired](https://github.com/runspired)) - * [#8733](https://github.com/emberjs/data/pull/8733) chore: improve types and lint ([@runspired](https://github.com/runspired)) - * [#8727](https://github.com/emberjs/data/pull/8727) chore: fix peers and get perf-test-app running again ([@runspired](https://github.com/runspired)) - * [#8717](https://github.com/emberjs/data/pull/8717) Switch from local and @types/ember types to ember-source types ([@BradBarnich](https://github.com/BradBarnich)) - * [#8499](https://github.com/emberjs/data/pull/8499) chore: refactor model hook support to live in the model package ([@runspired](https://github.com/runspired)) - * [#8862](https://github.com/emberjs/data/pull/8862) chore: remove more runloop usage | completely remove rsvp ([@runspired](https://github.com/runspired)) - * [#8861](https://github.com/emberjs/data/pull/8861) chore: remove runloop usage ([@runspired](https://github.com/runspired)) - * [#8859](https://github.com/emberjs/data/pull/8859) chore: update target labels ([@runspired](https://github.com/runspired)) - * [#8858](https://github.com/emberjs/data/pull/8858) chore: update required labels ([@runspired](https://github.com/runspired)) - * [#8830](https://github.com/emberjs/data/pull/8830) chore: cleanup actions/setup usage ([@runspired](https://github.com/runspired)) - * [#8812](https://github.com/emberjs/data/pull/8812) fix typo ([@samridhivig](https://github.com/samridhivig)) - * [#8802](https://github.com/emberjs/data/pull/8802) chore: fix fastboot-testing deprecation ([@runspired](https://github.com/runspired)) - * [#8801](https://github.com/emberjs/data/pull/8801) chore: resolve deprecation in fastboot app ([@runspired](https://github.com/runspired)) - * [#8860](https://github.com/emberjs/data/pull/8860) chore: burn down runloop and RSVP usage ([@runspired](https://github.com/runspired)) - * [#8832](https://github.com/emberjs/data/pull/8832) chore: add recommended JSON:API setup test app ([@runspired](https://github.com/runspired)) - * [#8829](https://github.com/emberjs/data/pull/8829) chore: eliminate dead build code ([@runspired](https://github.com/runspired)) - * [#8823](https://github.com/emberjs/data/pull/8823) fix: graph instantiation should not be required ([@runspired](https://github.com/runspired)) - -#### Committers: 11 -- Bert De Block ([@bertdeblock](https://github.com/bertdeblock)) -- Chris Thoburn ([@runspired](https://github.com/runspired)) -- Daniel Gröger ([@dagroe](https://github.com/dagroe)) -- Kirill Shaplyko ([@Baltazore](https://github.com/Baltazore)) -- Patrick Pircher ([@patricklx](https://github.com/patricklx)) -- Sam Van Campenhout ([@Windvis](https://github.com/Windvis)) -- Samridhi Vig ([@samridhivig](https://github.com/samridhivig)) -- Brad Barnich ([@BradBarnich](https://github.com/BradBarnich)) -- Jon Johnson ([@jrjohnson](https://github.com/jrjohnson)) -- Michal Bryxí ([@MichalBryxi](https://github.com/MichalBryxi)) -- Peter Wagenet ([@wagenet](https://github.com/wagenet)) - -## 5.2.0 (2023-08-17) - -* Re-release of 5.1.2 to keep lockstep pace. This release contains no new work. -## 5.1.2 (2023-08-17) -#### :bug: Bug Fix - -* [#8750](https://github.com/emberjs/data/pull/8750) Backport into release ([@jrjohnson](https://github.com/jrjohnson)) - * fix: @ember-data/debug should declare its peer-dependency on @ember-data/store #8703 - * fix: de-dupe coalescing when includes or adapterOptions is present but still use findRecord #8704 - * fix: make implicit relationship teardown following delete of related record safe #8705 - * fix: catch errors during didCommit in DEBUG #8708 - -## 5.1.1 (2023-07-07) - -#### :bug: Bug Fix - * [#8685](https://github.com/emberjs/data/pull/8685) fix: unloadAll(void) should not destroy the notification manager (backports #8684) ([@runspired](https://github.com/runspired)) - -#### Committers: 1 -- Chris Thoburn ([@runspired](https://github.com/runspired)) - -## 5.1.0 (2023-06-29) - -#### :bug: Bug Fix - * [#8657](https://github.com/emberjs/data/pull/8657) fix: ensure deprecation configs are threaded to each package ([@runspired](https://github.com/runspired)) - * [#8649](https://github.com/emberjs/data/pull/8649) fix: NotificationManager should only invoke resource/document callbacks owned by the originating store ([@runspired](https://github.com/runspired)) - -#### Committers: 1 -- Chris Thoburn ([@runspired](https://github.com/runspired)) - -## 5.0.1 (2023-06-29) - -#### :bug: Bug Fix - * [#8649](https://github.com/emberjs/data/pull/8649) fix: NotificationManager should only invoke resource/document callbacks owned by the originating store ([@runspired](https://github.com/runspired)) - -#### Committers: 1 -- Chris Thoburn ([@runspired](https://github.com/runspired)) - -## 5.0.0 (2023-06-10) - -#### :bug: Bug Fix -* `adapter` - * [#8621](https://github.com/emberjs/data/pull/8621) fix: normalizeErrorResponse should be resilient to non-string details ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) -* Other - * [#8598](https://github.com/emberjs/data/pull/8598) fix: docs generation should maintain a stable relative path ([@runspired](https://github.com/runspired)) -* `json-api`, `legacy-compat`, `store` - * [#8566](https://github.com/emberjs/data/pull/8566) Avoid unnecessary identity notification when record is saved ([@robbytx](https://github.com/robbytx)) -* `model` - * [#8597](https://github.com/emberjs/data/pull/8597) fix: dont share promise cache for all fields ([@runspired](https://github.com/runspired)) -* `store` - * [#8594](https://github.com/emberjs/data/pull/8594) fix: restore Store extends EmberObject :( ([@runspired](https://github.com/runspired)) - * [#8570](https://github.com/emberjs/data/pull/8570) Fix: don't clear RecordArray if remaining record does not match the removed record ([@esbanarango](https://github.com/esbanarango)) -* `graph`, `model`, `private-build-infra` - * [#8555](https://github.com/emberjs/data/pull/8555) fix: fix polymorphic assertions when deprecated code is removed, improve polymorphic dx ([@runspired](https://github.com/runspired)) - -#### :shower: Deprecation Removal -* `-ember-data`, `adapter`, `debug`, `graph`, `json-api`, `legacy-compat`, `model`, `private-build-infra`, `store`, `unpublished-test-infra` - * [#8550](https://github.com/emberjs/data/pull/8550) chore: remove 4.x deprecations ([@runspired](https://github.com/runspired)) - -#### :memo: Documentation -* `store` - * [#8601](https://github.com/emberjs/data/pull/8601) docs: fix forgotten references to FetchManager ([@runspired](https://github.com/runspired)) -* Other - * [#8598](https://github.com/emberjs/data/pull/8598) fix: docs generation should maintain a stable relative path ([@runspired](https://github.com/runspired)) - -#### Committers: 4 -- Chris Thoburn ([@runspired](https://github.com/runspired)) -- Esteban ([@esbanarango](https://github.com/esbanarango)) -- Robby Morgan ([@robbytx](https://github.com/robbytx)) -- [@NullVoxPopuli](https://github.com/NullVoxPopuli) +# Ember Data Changelog ## v4.12.8 (2024-05-08) @@ -510,7 +71,7 @@ Eric Kelly ([@HeroicEric](https://github.com/HeroicEric)) #### Committers: 1 - Chris Thoburn ([@runspired](https://github.com/runspired)) -## LTS 4.12.2 (2023-07-07) +## 4.12.2 (2023-07-07) #### :rocket: Enhancement * [#8660](https://github.com/emberjs/data/pull/8660) DX: Nicer backtracking errors ([@runspired](https://github.com/runspired)) @@ -521,7 +82,7 @@ Eric Kelly ([@HeroicEric](https://github.com/HeroicEric)) #### Committers: 1 - Chris Thoburn ([@runspired](https://github.com/runspired)) -## LTS 4.12.1 (2023-06-29) +## 4.12.1 (2023-06-29) #### :bug: Bug Fix * [#8656](https://github.com/emberjs/data/pull/8656) fix: NotificationManager should only invoke resource/document callbacks owned by the originating store (#8649) ([@runspired](https://github.com/runspired)) @@ -533,6 +94,7 @@ Eric Kelly ([@HeroicEric](https://github.com/HeroicEric)) - Chris Thoburn ([@runspired](https://github.com/runspired)) - Esteban ([@esbanarango](https://github.com/esbanarango)) + ## 4.12.0 (2023-04-06) #### :rocket: Enhancement @@ -884,46 +446,6 @@ This is a re-release of 4.10.0 - [@law-rence](https://github.com/law-rence) - Eugen Ciur ([@ciur](https://github.com/ciur)) -## v4.6.5 (2024-05-08) - -#### :bug: Bug Fix -* [#9316](https://github.com/emberjs/data/pull/9316) Notify on length when notifying that many-array has changed - -#### Committers: 1 -- -Ross Grayton ([@grayt0r](https://github.com/grayt0r)) - - -## v4.6.4 (2022-10-02) - -#### :bug: Bug Fix -* `private-build-infra` - * [#8199](https://github.com/emberjs/data/pull/8199) [backport release-prev] fix: thread polyfillUUID config through nested deps ([@runspired](https://github.com/runspired)) - -#### Committers: 1 -- Chris Thoburn ([@runspired](https://github.com/runspired)) - -## v4.6.3 (2022-09-15) - -#### :bug: Bug Fix -* `store` - * fix: allow ManyArray being passed to createRecord - -## v4.6.2 (2022-09-15) - -#### :bug: Bug Fix -* `store` - * [#8169](https://github.com/emberjs/data/pull/8169) fix: uuid polyfill logic ([@jrjohnson](https://github.com/jrjohnson)) -* `-ember-data`, `model` - * [#8148](https://github.com/emberjs/data/pull/8148) Clear subscriptions once unsubscribed, don't unnecessarily churn on subscriptions ([@jrjohnson](https://github.com/jrjohnson)) -* `private-build-infra` - * [#8145](https://github.com/emberjs/data/pull/8145) fix earlier versions of node-14 (#8108) ([@jrjohnson](https://github.com/jrjohnson)) -* `private-build-infra`, `store` - * [#8144](https://github.com/emberjs/data/pull/8144) Backport add optional polyfill (#8109) ([@jrjohnson](https://github.com/jrjohnson)) - -#### Committers: 1 -- Jon Johnson ([@jrjohnson](https://github.com/jrjohnson)) - ## v4.6.1 (2022-07-28) #### :bug: Bug Fix @@ -1034,15 +556,6 @@ Ross Grayton ([@grayt0r](https://github.com/grayt0r)) - Cameron Dubas ([@camerondubas](https://github.com/camerondubas)) - Jen Weber ([@jenweber](https://github.com/jenweber)) -## v4.4.2 (2023-08-01) - -#### :bug: Bug Fix -* `model` - * [#8713](https://github.com/emberjs/data/pull/8713) Notify on length when notifying that many-array has changed ([@richgt](https://github.com/richgt)) - -#### Committers: 1 -- Rich Glazerman ([@richgt](https://github.com/richgt)) - ## v4.1.0 (2021-12-30) #### :house: Internal @@ -3418,7 +2931,7 @@ The full API reference of `DS.Snapshot` can be found [here](https://api.emberjs. - fetch() -> fetchById() in docs - Run findHasMany inside an ED runloop - Cleanup debug adapter test: Watching Records -- Fixed didDelete event/callback not fired in uncommitted state +- Fixed didDelete event/callback not fired in uncommited state - Add main entry point for package.json. - register the store as a service - Warn when expected coalesced records are not found in the response diff --git a/config/package.json b/config/package.json index 1f04ca8e2bf..38efe8f7314 100644 --- a/config/package.json +++ b/config/package.json @@ -1,7 +1,7 @@ { "name": "@warp-drive/internal-config", "private": true, - "version": "5.4.0-alpha.121", + "version": "4.12.8", "type": "module", "dependencies": { "@babel/cli": "^7.24.5", diff --git a/package.json b/package.json index 6b001446895..e438083270f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "license": "MIT", "repository": { diff --git a/packages/-ember-data/app/adapters/-json-api.js b/packages/-ember-data/app/adapters/-json-api.js new file mode 100644 index 00000000000..2cbb7cd7057 --- /dev/null +++ b/packages/-ember-data/app/adapters/-json-api.js @@ -0,0 +1 @@ +export { default } from '@ember-data/adapter/json-api'; diff --git a/packages/-ember-data/app/initializers/ember-data.js b/packages/-ember-data/app/initializers/ember-data.js index 4c8e1d14e7b..a3dd9a3a6f5 100644 --- a/packages/-ember-data/app/initializers/ember-data.js +++ b/packages/-ember-data/app/initializers/ember-data.js @@ -1,12 +1,11 @@ -import '@ember-data/request-utils/deprecation-support'; +import 'ember-data'; + +import setupContainer from 'ember-data/setup-container'; /* This code initializes EmberData in an Ember application. */ export default { name: 'ember-data', - initialize(application) { - application.registerOptionsForType('serializer', { singleton: false }); - application.registerOptionsForType('adapter', { singleton: false }); - }, + initialize: setupContainer, }; diff --git a/packages/-ember-data/app/instance-initializers/ember-data.js b/packages/-ember-data/app/instance-initializers/ember-data.js new file mode 100644 index 00000000000..b48556a316c --- /dev/null +++ b/packages/-ember-data/app/instance-initializers/ember-data.js @@ -0,0 +1,5 @@ +/* exists only for things that historically used "after" or "before" */ +export default { + name: 'ember-data', + initialize() {}, +}; diff --git a/packages/-ember-data/app/serializers/-default.js b/packages/-ember-data/app/serializers/-default.js new file mode 100644 index 00000000000..d617bfb1824 --- /dev/null +++ b/packages/-ember-data/app/serializers/-default.js @@ -0,0 +1 @@ +export { default } from '@ember-data/serializer/json'; diff --git a/packages/-ember-data/app/serializers/-json-api.js b/packages/-ember-data/app/serializers/-json-api.js new file mode 100644 index 00000000000..59723c5ab2a --- /dev/null +++ b/packages/-ember-data/app/serializers/-json-api.js @@ -0,0 +1 @@ +export { default } from '@ember-data/serializer/json-api'; diff --git a/packages/-ember-data/app/serializers/-rest.js b/packages/-ember-data/app/serializers/-rest.js new file mode 100644 index 00000000000..d6878ba3c3e --- /dev/null +++ b/packages/-ember-data/app/serializers/-rest.js @@ -0,0 +1 @@ +export { default } from '@ember-data/serializer/rest'; diff --git a/packages/-ember-data/app/services/store.js b/packages/-ember-data/app/services/store.js index 94f7019f584..043aebdc25a 100644 --- a/packages/-ember-data/app/services/store.js +++ b/packages/-ember-data/app/services/store.js @@ -1,10 +1,11 @@ import { deprecate } from '@ember/debug'; export { default } from 'ember-data/store'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; deprecate( "You are relying on ember-data auto-magically installing the store service. Use `export { default } from 'ember-data/store';` in app/services/store.js instead", - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/app/transforms/boolean.js b/packages/-ember-data/app/transforms/boolean.js index be707e054f2..764286175bc 100644 --- a/packages/-ember-data/app/transforms/boolean.js +++ b/packages/-ember-data/app/transforms/boolean.js @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { BooleanTransform as default } from '@ember-data/serializer/transform'; deprecate( "You are relying on ember-data auto-magically installing the BooleanTransform. Use `export { BooleanTransform as default } from '@ember-data/serializer/transform';` in app/transforms/boolean.js instead", - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/app/transforms/date.js b/packages/-ember-data/app/transforms/date.js index 1ba53723825..187c01d2e40 100644 --- a/packages/-ember-data/app/transforms/date.js +++ b/packages/-ember-data/app/transforms/date.js @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { DateTransform as default } from '@ember-data/serializer/transform'; deprecate( "You are relying on ember-data auto-magically installing the DateTransform. Use `export { DateTransform as default } from '@ember-data/serializer/transform';` in app/transforms/date.js instead", - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/app/transforms/number.js b/packages/-ember-data/app/transforms/number.js index e33eac7b1a4..c79ef0d21dd 100644 --- a/packages/-ember-data/app/transforms/number.js +++ b/packages/-ember-data/app/transforms/number.js @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { NumberTransform as default } from '@ember-data/serializer/transform'; deprecate( "You are relying on ember-data auto-magically installing the NumberTransform. Use `export { NumberTransform as default } from '@ember-data/serializer/transform';` in app/transforms/number.js instead", - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/app/transforms/string.js b/packages/-ember-data/app/transforms/string.js index 95d95453178..929095af070 100644 --- a/packages/-ember-data/app/transforms/string.js +++ b/packages/-ember-data/app/transforms/string.js @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { StringTransform as default } from '@ember-data/serializer/transform'; deprecate( "You are relying on ember-data auto-magically installing the StringTransform. Use `export { StringTransform as default } from '@ember-data/serializer/transform';` in app/transforms/string.js instead", - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/package.json b/packages/-ember-data/package.json index b31164fded4..77a99520768 100644 --- a/packages/-ember-data/package.json +++ b/packages/-ember-data/package.json @@ -1,6 +1,6 @@ { "name": "ember-data", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "description": "The lightweight reactive data library for JavaScript applications", "keywords": [ "ember-addon" diff --git a/packages/-ember-data/src/-private/index.ts b/packages/-ember-data/src/-private/index.ts index d677821855a..ef3ed492d25 100644 --- a/packages/-ember-data/src/-private/index.ts +++ b/packages/-ember-data/src/-private/index.ts @@ -4,15 +4,21 @@ import { deprecate } from '@ember/debug'; import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; import ObjectProxy from '@ember/object/proxy'; -deprecate('Importing from `ember-data/-private` is deprecated without replacement.', false, { - id: 'ember-data:deprecate-legacy-imports', - for: 'ember-data', - until: '6.0', - since: { - enabled: '5.2', - available: '4.13', - }, -}); +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + +deprecate( + 'Importing from `ember-data/-private` is deprecated without replacement.', + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:deprecate-legacy-imports', + for: 'ember-data', + until: '6.0', + since: { + enabled: '5.2', + available: '4.13', + }, + } +); export { default as Store } from '../store'; diff --git a/packages/-ember-data/src/adapter.ts b/packages/-ember-data/src/adapter.ts index 86e9e39f16c..7f870c613c2 100644 --- a/packages/-ember-data/src/adapter.ts +++ b/packages/-ember-data/src/adapter.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/adapter'; deprecate( 'Importing from `ember-data/adapter` is deprecated. Please import from `@ember-data/adapter` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/adapters/errors.ts b/packages/-ember-data/src/adapters/errors.ts index ef0abcbe475..ffa8d144a6a 100644 --- a/packages/-ember-data/src/adapters/errors.ts +++ b/packages/-ember-data/src/adapters/errors.ts @@ -1,5 +1,7 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { AbortError, default as AdapterError, @@ -10,11 +12,13 @@ export { ServerError, TimeoutError, UnauthorizedError, + errorsArrayToHash, + errorsHashToArray, } from '@ember-data/adapter/error'; deprecate( 'Importing from `ember-data/adapters/errors` is deprecated. Please import from `@ember-data/adapter` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/adapters/json-api.ts b/packages/-ember-data/src/adapters/json-api.ts index 5b02150cc2b..d056eea91f9 100644 --- a/packages/-ember-data/src/adapters/json-api.ts +++ b/packages/-ember-data/src/adapters/json-api.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/adapter/json-api'; deprecate( 'Importing from `ember-data/adapters/json-api` is deprecated. Please import from `@ember-data/adapter/json-api` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/adapters/rest.ts b/packages/-ember-data/src/adapters/rest.ts index 3e8d5887c90..7bea4ec2261 100644 --- a/packages/-ember-data/src/adapters/rest.ts +++ b/packages/-ember-data/src/adapters/rest.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/adapter/rest'; deprecate( 'Importing from `ember-data/adapters/rest` is deprecated. Please import from `@ember-data/adapter/rest` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/attr.ts b/packages/-ember-data/src/attr.ts index d4d29c0318a..93b5cd32c1e 100644 --- a/packages/-ember-data/src/attr.ts +++ b/packages/-ember-data/src/attr.ts @@ -1,13 +1,19 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { attr as default } from '@ember-data/model'; -deprecate('Importing from `ember-data/attr` is deprecated. Please import from `@ember-data/model` instead.', false, { - id: 'ember-data:deprecate-legacy-imports', - for: 'ember-data', - until: '6.0', - since: { - enabled: '5.2', - available: '4.13', - }, -}); +deprecate( + 'Importing from `ember-data/attr` is deprecated. Please import from `@ember-data/model` instead.', + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:deprecate-legacy-imports', + for: 'ember-data', + until: '6.0', + since: { + enabled: '5.2', + available: '4.13', + }, + } +); diff --git a/packages/-ember-data/src/index.ts b/packages/-ember-data/src/index.ts index 6db2ee494c9..87d64f7b851 100644 --- a/packages/-ember-data/src/index.ts +++ b/packages/-ember-data/src/index.ts @@ -185,6 +185,7 @@ import Transform, { NumberTransform, StringTransform, } from '@ember-data/serializer/transform'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; import { DS, @@ -201,7 +202,7 @@ import setupContainer from './setup-container'; deprecate( 'Importing from `ember-data` is deprecated. Please import from the appropriate `@ember-data/*` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/model.ts b/packages/-ember-data/src/model.ts index f27c9832bca..4164b3a5dd1 100644 --- a/packages/-ember-data/src/model.ts +++ b/packages/-ember-data/src/model.ts @@ -1,13 +1,19 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/model'; -deprecate('Importing from `ember-data/model` is deprecated. Please import from `@ember-data/model` instead.', false, { - id: 'ember-data:deprecate-legacy-imports', - for: 'ember-data', - until: '6.0', - since: { - enabled: '5.2', - available: '4.13', - }, -}); +deprecate( + 'Importing from `ember-data/model` is deprecated. Please import from `@ember-data/model` instead.', + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:deprecate-legacy-imports', + for: 'ember-data', + until: '6.0', + since: { + enabled: '5.2', + available: '4.13', + }, + } +); diff --git a/packages/-ember-data/src/relationships.ts b/packages/-ember-data/src/relationships.ts index 24578d4944c..5c5cabfbf14 100644 --- a/packages/-ember-data/src/relationships.ts +++ b/packages/-ember-data/src/relationships.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { belongsTo, hasMany } from '@ember-data/model'; deprecate( 'Importing from `ember-data/relationships` is deprecated. Please import from `@ember-data/model` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/serializer.ts b/packages/-ember-data/src/serializer.ts index 33c1b590691..6c878eaeccb 100644 --- a/packages/-ember-data/src/serializer.ts +++ b/packages/-ember-data/src/serializer.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/serializer'; deprecate( 'Importing from `ember-data/serializer` is deprecated. Please import from `@ember-data/serializer` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/serializers/embedded-records-mixin.ts b/packages/-ember-data/src/serializers/embedded-records-mixin.ts index 8cc22d64b6f..6e5ad9a040f 100644 --- a/packages/-ember-data/src/serializers/embedded-records-mixin.ts +++ b/packages/-ember-data/src/serializers/embedded-records-mixin.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { EmbeddedRecordsMixin as default } from '@ember-data/serializer/rest'; deprecate( 'Importing from `ember-data/serializers/embedded-records-mixin` is deprecated. Please import from `@ember-data/serializer/rest` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/serializers/json-api.ts b/packages/-ember-data/src/serializers/json-api.ts index 272e7f8deb5..cd8bb715d8f 100644 --- a/packages/-ember-data/src/serializers/json-api.ts +++ b/packages/-ember-data/src/serializers/json-api.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/serializer/json-api'; deprecate( 'Importing from `ember-data/serializers/json-api` is deprecated. Please import from `@ember-data/serializer/json-api` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/serializers/json.ts b/packages/-ember-data/src/serializers/json.ts index 3148ab0ff37..46fccf1e318 100644 --- a/packages/-ember-data/src/serializers/json.ts +++ b/packages/-ember-data/src/serializers/json.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/serializer/json'; deprecate( 'Importing from `ember-data/serializers/json` is deprecated. Please import from `@ember-data/serializer/json` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/serializers/rest.ts b/packages/-ember-data/src/serializers/rest.ts index 41743413946..4c65661a043 100644 --- a/packages/-ember-data/src/serializers/rest.ts +++ b/packages/-ember-data/src/serializers/rest.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/serializer/rest'; deprecate( 'Importing from `ember-data/serializers/rest` is deprecated. Please import from `@ember-data/serializer/rest` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/-ember-data/src/setup-container.ts b/packages/-ember-data/src/setup-container.ts index f6c010d3548..00a519ce955 100644 --- a/packages/-ember-data/src/setup-container.ts +++ b/packages/-ember-data/src/setup-container.ts @@ -1,6 +1,8 @@ import type Application from '@ember/application'; import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + function initializeStore(application: Application) { application.registerOptionsForType('serializer', { singleton: false }); application.registerOptionsForType('adapter', { singleton: false }); @@ -10,12 +12,16 @@ export default function setupContainer(application: Application) { initializeStore(application); } -deprecate('Importing from `ember-data/setup-container` is deprecated without replacement', false, { - id: 'ember-data:deprecate-legacy-imports', - for: 'ember-data', - until: '6.0', - since: { - enabled: '5.2', - available: '4.13', - }, -}); +deprecate( + 'Importing from `ember-data/setup-container` is deprecated without replacement', + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:deprecate-legacy-imports', + for: 'ember-data', + until: '6.0', + since: { + enabled: '5.2', + available: '4.13', + }, + } +); diff --git a/packages/-ember-data/src/store.ts b/packages/-ember-data/src/store.ts index 7482209de23..d750281c67e 100644 --- a/packages/-ember-data/src/store.ts +++ b/packages/-ember-data/src/store.ts @@ -24,6 +24,12 @@ function hasRequestManager(store: BaseStore): boolean { return 'requestManager' in store; } +// FIXME @ember-data/store +// may also need to do all of this configuration +// because in 4.12 we had not yet caused it to be +// required to use `ember-data/store` to get the configured +// store except in the case of RequestManager. +// so for instance in tests new Store would mostly just work (tm) export default class Store extends BaseStore { declare _fetchManager: FetchManager; diff --git a/packages/-ember-data/src/transform.ts b/packages/-ember-data/src/transform.ts index ca0fb52b174..c9124e01d93 100644 --- a/packages/-ember-data/src/transform.ts +++ b/packages/-ember-data/src/transform.ts @@ -1,10 +1,12 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; + export { default } from '@ember-data/serializer/transform'; deprecate( 'Importing from `ember-data/transform` is deprecated. Please import from `@ember-data/serializer/transform` instead.', - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-legacy-imports', for: 'ember-data', diff --git a/packages/active-record/package.json b/packages/active-record/package.json index 606449b4670..c851420fdae 100644 --- a/packages/active-record/package.json +++ b/packages/active-record/package.json @@ -1,7 +1,7 @@ { "name": "@ember-data/active-record", "description": "ActiveRecord Format Support for EmberData", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": false, "license": "MIT", "author": "Chris Thoburn ", diff --git a/packages/adapter/package.json b/packages/adapter/package.json index 677bc93ed5a..90f0527529c 100644 --- a/packages/adapter/package.json +++ b/packages/adapter/package.json @@ -1,6 +1,6 @@ { "name": "@ember-data/adapter", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "description": "Provides Legacy JSON:API and REST Implementations of the Adapter Interface for use with @ember-data/store", "keywords": [ "ember-addon" diff --git a/packages/adapter/src/error.js b/packages/adapter/src/error.js index f19e7149b9d..d21456c1334 100644 --- a/packages/adapter/src/error.js +++ b/packages/adapter/src/error.js @@ -1,6 +1,9 @@ /** @module @ember-data/adapter/error */ +import { deprecate } from '@ember/debug'; + +import { DEPRECATE_HELPERS } from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; import { getOrSetGlobal } from '@warp-drive/core-types/-private'; @@ -353,3 +356,161 @@ export const ServerError = getOrSetGlobal( extend(AdapterError, 'The adapter operation failed due to a server error') ); ServerError.prototype.code = 'ServerError'; + +function makeArray(value) { + return Array.isArray(value) ? value : [value]; +} + +const SOURCE_POINTER_REGEXP = /^\/?data\/(attributes|relationships)\/(.*)/; +const SOURCE_POINTER_PRIMARY_REGEXP = /^\/?data/; +const PRIMARY_ATTRIBUTE_KEY = 'base'; +/** + Convert an hash of errors into an array with errors in JSON-API format. + ```javascript + import { errorsHashToArray } from '@ember-data/adapter/error'; + + let errors = { + base: 'Invalid attributes on saving this record', + name: 'Must be present', + age: ['Must be present', 'Must be a number'] + }; + let errorsArray = errorsHashToArray(errors); + // [ + // { + // title: "Invalid Document", + // detail: "Invalid attributes on saving this record", + // source: { pointer: "/data" } + // }, + // { + // title: "Invalid Attribute", + // detail: "Must be present", + // source: { pointer: "/data/attributes/name" } + // }, + // { + // title: "Invalid Attribute", + // detail: "Must be present", + // source: { pointer: "/data/attributes/age" } + // }, + // { + // title: "Invalid Attribute", + // detail: "Must be a number", + // source: { pointer: "/data/attributes/age" } + // } + // ] + ``` + @method errorsHashToArray + @for @ember-data/adapter/error + @static + @deprecated + @public + @param {Object} errors hash with errors as properties + @return {Array} array of errors in JSON-API format +*/ +export function errorsHashToArray(errors) { + if (DEPRECATE_HELPERS) { + deprecate(`errorsHashToArray helper has been deprecated.`, false, { + id: 'ember-data:deprecate-errors-hash-to-array-helper', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + }); + const out = []; + + if (errors) { + Object.keys(errors).forEach((key) => { + const messages = makeArray(errors[key]); + for (let i = 0; i < messages.length; i++) { + let title = 'Invalid Attribute'; + let pointer = `/data/attributes/${key}`; + if (key === PRIMARY_ATTRIBUTE_KEY) { + title = 'Invalid Document'; + pointer = `/data`; + } + out.push({ + title: title, + detail: messages[i], + source: { + pointer: pointer, + }, + }); + } + }); + } + + return out; + } + assert(`errorsHashToArray helper has been removed`); +} + +/** + Convert an array of errors in JSON-API format into an object. + + ```javascript + import { errorsArrayToHash } from '@ember-data/adapter/error'; + + let errorsArray = [ + { + title: 'Invalid Attribute', + detail: 'Must be present', + source: { pointer: '/data/attributes/name' } + }, + { + title: 'Invalid Attribute', + detail: 'Must be present', + source: { pointer: '/data/attributes/age' } + }, + { + title: 'Invalid Attribute', + detail: 'Must be a number', + source: { pointer: '/data/attributes/age' } + } + ]; + + let errors = errorsArrayToHash(errorsArray); + // { + // "name": ["Must be present"], + // "age": ["Must be present", "must be a number"] + // } + ``` + + @method errorsArrayToHash + @static + @for @ember-data/adapter/error + @deprecated + @public + @param {Array} errors array of errors in JSON-API format + @return {Object} +*/ +export function errorsArrayToHash(errors) { + if (DEPRECATE_HELPERS) { + deprecate(`errorsArrayToHash helper has been deprecated.`, false, { + id: 'ember-data:deprecate-errors-array-to-hash-helper', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + }); + const out = {}; + + if (errors) { + errors.forEach((error) => { + if (error.source && error.source.pointer) { + let key = error.source.pointer.match(SOURCE_POINTER_REGEXP); + + if (key) { + key = key[2]; + } else if (error.source.pointer.search(SOURCE_POINTER_PRIMARY_REGEXP) !== -1) { + key = PRIMARY_ATTRIBUTE_KEY; + } + + if (key) { + out[key] = out[key] || []; + out[key].push(error.detail || error.title); + } + } + }); + } + + return out; + } + assert(`errorsArrayToHash helper has been removed`); +} diff --git a/packages/build-config/package.json b/packages/build-config/package.json index da8c7cc9d27..297950862a9 100644 --- a/packages/build-config/package.json +++ b/packages/build-config/package.json @@ -1,6 +1,6 @@ { "name": "@warp-drive/build-config", - "version": "0.0.0-alpha.58", + "version": "4.12.8", "description": "Provides Build Configuration for projects using WarpDrive or EmberData", "keywords": [ "ember-data", diff --git a/packages/build-config/src/deprecation-versions.ts b/packages/build-config/src/deprecation-versions.ts index 78d21a10057..80d5148b4d3 100644 --- a/packages/build-config/src/deprecation-versions.ts +++ b/packages/build-config/src/deprecation-versions.ts @@ -88,6 +88,621 @@ * @public */ export const DEPRECATE_CATCH_ALL = '99.0'; + +/** + * **id: ember-data:rsvp-unresolved-async** + * + * Deprecates when a request promise did not resolve prior to the store tearing down. + * + * Note: in most cases even with the promise guard that is now being deprecated + * a test crash would still be encountered. + * + * To resolve: Tests or Fastboot instances which crash need to find triggers requests + * and properly await them before tearing down. + * + * @property DEPRECATE_RSVP_PROMISE + * @since 4.4 + * @until 5.0 + * @public + */ +export const DEPRECATE_RSVP_PROMISE = '4.4'; + +/** + * **id: ember-data:model-save-promise** + * + * Affects + * - model.save / store.saveRecord + * - model.reload + * + * Deprecates the promise-proxy returned by these methods in favor of + * a Promise return value. + * + * To resolve this deprecation, `await` or `.then` the return value + * before doing work with the result instead of accessing values via + * the proxy. + * + * To continue utilizing flags such as `isPending` in your templates + * consider using [ember-promise-helpers](https://github.com/fivetanley/ember-promise-helpers) + * + * @property DEPRECATE_SAVE_PROMISE_ACCESS + * @since 4.4 + * @until 5.0 + * @public + */ +export const DEPRECATE_SAVE_PROMISE_ACCESS = '4.4'; + +/** + * **id: ember-data:deprecate-snapshot-model-class-access** + * + * Deprecates accessing the factory class for a given resource type + * via properties on various classes. + * + * Guards + * + * - SnapshotRecordArray.type + * - Snapshot.type + * - RecordArray.type + * + * Use `store.modelFor()` instead. + * + * @property DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS + * @since 4.5 + * @until 5.0 + * @public + */ +export const DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS = '4.5'; + +/** + * **id: ember-data:deprecate-store-find** + * + * Deprecates using `store.find` instead of `store.findRecord`. Typically + * `store.find` is a mistaken call that occurs when using implicit route behaviors + * in Ember which attempt to derive how to load data via parsing the route params + * for a route which does not implement a `model` hook. + * + * To resolve, use `store.findRecord`. This may require implementing an associated + * route's `model() {}` hook. + * + * @property DEPRECATE_STORE_FIND + * @since 4.5 + * @until 5.0 + * @public + */ +export const DEPRECATE_STORE_FIND = '4.5'; + +/** + * **id: ember-data:deprecate-has-record-for-id** + * + * Deprecates `store.hasRecordForId(type, id)` in favor of `store.peekRecord({ type, id }) !== null`. + * + * Broadly speaking, while the ability to query for presence is important, a key distinction exists + * between these methods that make relying on `hasRecordForId` unsafe, as it may report `true` for a + * record which is not-yet loaded and un-peekable. `peekRecord` offers a safe mechanism by which to check + * for whether a record is present in a usable manner. + * + * @property DEPRECATE_HAS_RECORD + * @since 4.5 + * @until 5.0 + * @public + */ +export const DEPRECATE_HAS_RECORD = '4.5'; + +/** + * **id: ember-data:deprecate-string-arg-schemas** + * + * Deprecates `schema.attributesDefinitionFor(type)` and + * `schema.relationshipsDefinitionFor(type)` in favor of + * a consistent object signature (`identifier | { type }`). + * + * To resolve change + * + * ```diff + * - store.getSchemaDefinitionService().attributesDefinitionFor('user') + * + store.getSchemaDefinitionService().attributesDefinitionFor({ type: 'user' }) + * ``` + * + * @property DEPRECATE_STRING_ARG_SCHEMAS + * @since 4.5 + * @until 5.0 + * @public + */ +export const DEPRECATE_STRING_ARG_SCHEMAS = '4.5'; + +/** + * **id: ember-data:deprecate-secret-adapter-fallback** + * + * Deprecates the secret `-json-api` fallback adapter in favor + * or an explicit "catch all" application adapter. In addition + * to this deprecation ensuring the user has explicitly chosen an + * adapter, this ensures that the user may choose to use no adapter + * at all. + * + * Simplest fix: + * + * */app/adapters/application.js* + * ```js + * export { default } from '@ember-data/adapter/json-api'; + * ``` + * + * @property DEPRECATE_JSON_API_FALLBACK + * @since 4.5 + * @until 5.0 + * @public + */ +export const DEPRECATE_JSON_API_FALLBACK = '4.5'; + +/** + * **id: ember-data:deprecate-model-reopen** + * + * ---- + * + * For properties known ahead of time, instead of + * + * ```ts + * class User extends Model { @attr firstName; } + * + * User.reopen({ lastName: attr() }); + * ``` + * + * Extend `User` again or include it in the initial definition. + * + * ```ts + * class User extends Model { @attr firstName; @attr lastName } + * ``` + * + * For properties generated dynamically, consider registering + * a `SchemaDefinitionService` with the store , as such services + * are capable of dynamically adjusting their schemas, and utilize + * the `instantiateRecord` hook to create a Proxy based class that + * can react to the changes in the schema. + * + * + * Use Foo extends Model to extend your class instead + * + * + * + * + * **id: ember-data:deprecate-model-reopenclass** + * + * ---- + * + * Instead of reopenClass, define `static` properties with native class syntax + * or add them to the final object. + * + * ```ts + * // instead of + * User.reopenClass({ aStaticMethod() {} }); + * + * // do this + * class User { + * static aStaticMethod() {} + * } + * + * // or do this + * User.aStaticMethod = function() {} + * ``` + * + * + * @property DEPRECATE_MODEL_REOPEN + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_MODEL_REOPEN = '4.7'; + +/** + * **id: ember-data:deprecate-early-static** + * + * This deprecation triggers if static computed properties + * or methods are triggered without looking up the record + * via the store service's `modelFor` hook. Accessing this + * static information without looking up the model via the + * store most commonly occurs when + * + * - using ember-cli-mirage (to fix, refactor to not use its auto-discovery of ember-data models) + * - importing a model class and accessing its static information via the import + * + * Instead of + * + * ```js + * import User from 'my-app/models/user'; + * + * const relationships = User.relationshipsByName; + * ``` + * + * Do *at least* this + * + * ```js + * const relationships = store.modelFor('user').relationshipsByName; + * ``` + * + * However, the much more future proof refactor is to not use `modelFor` at all but instead + * to utilize the schema service for this static information. + * + * ```js + * const relationships = store.getSchemaDefinitionService().relationshipsDefinitionFor({ type: 'user' }); + * ``` + * + * + * @property DEPRECATE_EARLY_STATIC + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_EARLY_STATIC = '4.7'; + +/** + * **id: ember-data:deprecate-errors-hash-to-array-helper** + * **id: ember-data:deprecate-errors-array-to-hash-helper** + * **id: ember-data:deprecate-normalize-modelname-helper** + * + * Deprecates `errorsHashToArray` `errorsArrayToHash` and `normalizeModelName` + * + * Users making use of these (already private) utilities can trivially copy them + * into their own codebase to continue using them, though we recommend refactoring + * to a more direct conversion into the expected errors format for the errors helpers. + * + * For refactoring normalizeModelName we also recommend following the guidance in + * [RFC#740 Deprecate Non-Strict Types](https://github.com/emberjs/rfcs/pull/740). + * + * + * @property DEPRECATE_HELPERS + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_HELPERS = '4.7'; + +/** + * **id: ember-data:deprecate-promise-many-array-behavior** + * + * [RFC Documentation](https://rfcs.emberjs.com/id/0745-ember-data-deprecate-methods-on-promise-many-array) + * + * This deprecation deprecates accessing values on the asynchronous proxy + * in favor of first "resolving" or "awaiting" the promise to retrieve a + * synchronous value. + * + * Template iteration of the asynchronous value will still work and not trigger + * the deprecation, but all JS access should be avoided and HBS access for anything + * but `{{#each}}` should also be refactored. + * + * Recommended approaches include using the addon `ember-promise-helpers`, using + * Ember's `resource` pattern (including potentially the addon `ember-data-resources`), + * resolving the value in routes/provider components, or using the references API. + * + * An example of using the [hasMany](https://api.emberjs.com/ember-data/4.11/classes/Model/methods/hasMany?anchor=hasMany) [reference API](https://api.emberjs.com/ember-data/release/classes/HasManyReference): + * + * ```ts + * // get the synchronous "ManyArray" value for the asynchronous "friends" relationship. + * // note, this will return `null` if the relationship has not been loaded yet + * const value = person.hasMany('friends').value(); + * + * // to get just the list of related IDs + * const ids = person.hasMany('friends').ids(); + * ``` + * + * References participate in autotracking and getters/cached getters etc. which consume them + * will recompute if the value changes. + * + * @property DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS = '4.7'; + +/** + * **id: ember-data:deprecate-non-strict-relationships** + * + * Deprecates when belongsTo and hasMany relationships are defined + * without specifying the inverse record's type. + * + * Instead of + * + * ```ts + * class Company extends Model { + * @hasMany() employees; + * } + * class Employee extends Model { + * @belongsTo() company; + * } + * ``` + * + * Use + * + * ```ts + * class Company extends Model { + * @hasMany('employee', { async: true, inverse: 'company' }) employees; + * } + * + * class Employee extends Model { + * @belongsTo('company', { async: true, inverse: 'employees' }) company; + * } + * ``` + * + * @property DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE = '4.7'; + +/** + * **id: ember-data:deprecate-non-strict-relationships** + * + * Deprecates when belongsTo and hasMany relationships are defined + * without specifying whether the relationship is asynchronous. + * + * The current behavior is that relationships which do not define + * this setting are aschronous (`{ async: true }`). + * + * Instead of + * + * ```ts + * class Company extends Model { + * @hasMany('employee') employees; + * } + * class Employee extends Model { + * @belongsTo('company') company; + * } + * ``` + * + * Use + * + * ```ts + * class Company extends Model { + * @hasMany('employee', { async: true, inverse: 'company' }) employees; + * } + * + * class Employee extends Model { + * @belongsTo('company', { async: true, inverse: 'employees' }) company; + * } + * ``` + * + * @property DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC = '4.7'; + +/** + * **id: ember-data:deprecate-non-strict-relationships** + * + * Deprecates when belongsTo and hasMany relationships are defined + * without specifying the inverse field on the related type. + * + * The current behavior is that relationships which do not define + * this setting have their inverse determined at runtime, which is + * potentially non-deterministic when mixins and polymorphism are involved. + * + * If an inverse relationship exists and you wish changes on one side to + * reflect onto the other side, use the inverse key. If you wish to not have + * changes reflected or no inverse relationship exists, specify `inverse: null`. + * + * Instead of + * + * ```ts + * class Company extends Model { + * @hasMany('employee') employees; + * } + * class Employee extends Model { + * @belongsTo('company') company; + * } + * ``` + * + * Use + * + * ```ts + * class Company extends Model { + * @hasMany('employee', { async: true, inverse: 'company' }) employees; + * } + * + * class Employee extends Model { + * @belongsTo('company', { async: true, inverse: 'employees' }) company; + * } + * ``` + * + * Instead of + * + * ```ts + * class Company extends Model { + * @hasMany('employee') employees; + * } + * class Employee extends Model { + * @attr name; + * } + * ``` + * + * Use + * + * ```ts + * class Company extends Model { + * @hasMany('employee', { async: true, inverse: null }) employees; + * } + * + * class Employee extends Model { + * @attr name; + * } + * ``` + * + * @property DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE = '4.7'; + +/** + * **id: ember-data:no-a-with-array-like** + * + * Deprecates when calling `A()` on an EmberData ArrayLike class + * is detected. This deprecation may not always trigger due to complexities + * in ember-source versions and the use (or disabling) of prototype extensions. + * + * To fix, just use the native array methods instead of the EmberArray methods + * and refrain from wrapping the array in `A()`. + * + * Note that some computed property macros may themselves utilize `A()`, in which + * scenario the computed properties need to be upgraded to octane syntax. + * + * For instance, instead of: + * + * ```ts + * class extends Component { + * @filterBy('items', 'isComplete') completedItems; + * } + * ``` + * + * Use the following: + * + * ```ts + * class extends Component { + * get completedItems() { + * return this.items.filter(item => item.isComplete); + * } + * } + * ``` + * + * @property DEPRECATE_A_USAGE + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_A_USAGE = '4.7'; + +/** + * **id: ember-data:deprecate-promise-proxies** + * + * Additional Reading: [RFC#846 Deprecate Proxies](https://rfcs.emberjs.com/id/0846-ember-data-deprecate-proxies) + * + * Deprecates using the proxy object/proxy array capabilities of values returned from + * + * - `store.findRecord` + * - `store.findAll` + * - `store.query` + * - `store.queryRecord` + * - `record.save` + * - `recordArray.save` + * - `recordArray.update` + * + * These methods will now return a native Promise that resolves with the value. + * + * Note that this does not deprecate the proxy behaviors of `PromiseBelongsTo`. See RFC for reasoning. + * The opportunity should still be taken if available to stop using these proxy behaviors; however, this class + * will remain until `import Model from '@ember-data/model';` is deprecated more broadly. + * + * @property DEPRECATE_PROMISE_PROXIES + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_PROMISE_PROXIES = '4.7'; + +/** + * **id: ember-data:deprecate-array-like** + * + * Deprecates Ember "Array-like" methods on RecordArray and ManyArray. + * + * These are the arrays returned respectively by `store.peekAll()`, `store.findAll()`and + * hasMany relationships on instance of Model or `record.hasMany('relationshipName').value()`. + * + * The appropriate refactor is to treat these arrays as native arrays and to use native array methods. + * + * For instance, instead of: + * + * ```ts + * users.firstObject; + * ``` + * + * Use: + * + * ```ts + * users[0]; + * // or + * users.at(0); + * ``` + * + * @property DEPRECATE_ARRAY_LIKE + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_ARRAY_LIKE = '4.7'; + +/** + * **id: ** + * + * This is a planned deprecation which will trigger when observer or computed + * chains are used to watch for changes on any EmberData RecordArray, ManyArray + * or PromiseManyArray. + * + * Support for these chains is currently guarded by the inactive deprecation flag + * listed here. + * + * @property DEPRECATE_COMPUTED_CHAINS + * @since 5.0 + * @until 6.0 + * @public + */ +export const DEPRECATE_COMPUTED_CHAINS = '5.0'; + +/** + * **id: ember-data:non-explicit-relationships** + * + * Deprecates when polymorphic relationships are detected via inheritance or mixins + * and no polymorphic relationship configuration has been setup. + * + * For further reading please review [RFC#793](https://rfcs.emberjs.com/id/0793-polymporphic-relations-without-inheritance) + * which introduced support for explicit relationship polymorphism without + * mixins or inheritance. + * + * You may still use mixins and inheritance to setup your polymorphism; however, the class + * structure is no longer what drives the design. Instead polymorphism is "traits" based or "structural": + * so long as each model which can satisfy the polymorphic relationship defines the inverse in the same + * way they work. + * + * Notably: `inverse: null` relationships can receive any type as a record with no additional configuration + * at all. + * + * Example Polymorphic Relationship Configuration + * + * ```ts + * // polymorphic relationship + * class Tag extends Model { + * @hasMany("taggable", { async: false, polymorphic: true, inverse: "tags" }) tagged; + * } + * + * // an inverse concrete relationship (e.g. satisfies "taggable") + * class Post extends Model { + * @hasMany("tag", { async: false, inverse: "tagged", as: "taggable" }) tags; + * } + * ``` + * + * @property DEPRECATE_NON_EXPLICIT_POLYMORPHISM + * @since 4.7 + * @until 5.0 + * @public + */ +export const DEPRECATE_NON_EXPLICIT_POLYMORPHISM = '4.7'; + +/** + * **id: ember-data:deprecate-many-array-duplicates** + * + * When the flag is `true` (default), adding duplicate records to a `ManyArray` + * is deprecated in non-production environments. In production environments, + * duplicate records added to a `ManyArray` will be deduped and no error will + * be thrown. + * + * When the flag is `false`, an error will be thrown when duplicates are added. + * + * @property DEPRECATE_MANY_ARRAY_DUPLICATES + * @since 5.3 + * @until 6.0 + * @public + */ +export const DEPRECATE_MANY_ARRAY_DUPLICATES = '4.12'; // '5.3'; + /** * **id: ember-data:deprecate-non-strict-types** * @@ -146,42 +761,6 @@ export const DEPRECATE_NON_STRICT_TYPES = '5.3'; */ export const DEPRECATE_NON_STRICT_ID = '5.3'; -/** - * **id: ** - * - * This is a planned deprecation which will trigger when observer or computed - * chains are used to watch for changes on any EmberData LiveArray, CollectionRecordArray, - * ManyArray or PromiseManyArray. - * - * Support for these chains is currently guarded by the deprecation flag - * listed here, enabling removal of the behavior if desired. - * - * @property DEPRECATE_COMPUTED_CHAINS - * @since 5.0 - * @until 6.0 - * @public - */ -export const DEPRECATE_COMPUTED_CHAINS = '5.0'; - -/** - * **id: ember-data:deprecate-legacy-imports** - * - * Deprecates when importing from `ember-data/*` instead of `@ember-data/*` - * in order to prepare for the eventual removal of the legacy `ember-data/*` - * - * All imports from `ember-data/*` should be updated to `@ember-data/*` - * except for `ember-data/store`. When you are using `ember-data` (as opposed to - * installing the indivudal packages) you should import from `ember-data/store` - * instead of `@ember-data/store` in order to receive the appropriate configuration - * of defaults. - * - * @property DEPRECATE_LEGACY_IMPORTS - * @since 5.3 - * @until 6.0 - * @public - */ -export const DEPRECATE_LEGACY_IMPORTS = '5.3'; - /** * **id: ember-data:deprecate-non-unique-collection-payloads** * @@ -372,23 +951,6 @@ export const DEPRECATE_NON_UNIQUE_PAYLOADS = '5.3'; */ export const DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE = '5.3'; -/** - * **id: ember-data:deprecate-many-array-duplicates** - * - * When the flag is `true` (default), adding duplicate records to a `ManyArray` - * is deprecated in non-production environments. In production environments, - * duplicate records added to a `ManyArray` will be deduped and no error will - * be thrown. - * - * When the flag is `false`, an error will be thrown when duplicates are added. - * - * @property DEPRECATE_MANY_ARRAY_DUPLICATES - * @since 5.3 - * @until 6.0 - * @public - */ -export const DEPRECATE_MANY_ARRAY_DUPLICATES = '5.3'; - /** * **id: ember-data:deprecate-store-extends-ember-object** * @@ -459,3 +1021,20 @@ export const ENABLE_LEGACY_SCHEMA_SERVICE = '5.4'; * @public */ export const DEPRECATE_EMBER_INFLECTOR = '5.3'; + +/** + * This is a special flag that can be used to opt-in early to receiving deprecations introduced in 5.x + * which have had their infra backported to 4.x versions of EmberData. + * + * When this flag is not present or set to `true`, the deprecations from the 5.x branch + * will not print their messages and the deprecation cannot be resolved. + * + * When this flag is present and set to `false`, the deprecations from the 5.x branch will + * print and can be resolved. + * + * @property DISABLE_6X_DEPRECATIONS + * @since 4.13 + * @until 5.0 + * @public + */ +export const DISABLE_6X_DEPRECATIONS = '6.0'; diff --git a/packages/build-config/src/deprecations.ts b/packages/build-config/src/deprecations.ts index e6c80aa73ea..344315da32f 100644 --- a/packages/build-config/src/deprecations.ts +++ b/packages/build-config/src/deprecations.ts @@ -1,12 +1,31 @@ // deprecations export const DEPRECATE_CATCH_ALL: boolean = true; +export const DEPRECATE_SAVE_PROMISE_ACCESS: boolean = true; +export const DEPRECATE_RSVP_PROMISE: boolean = true; +export const DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS: boolean = true; +export const DEPRECATE_STORE_FIND: boolean = true; +export const DEPRECATE_HAS_RECORD: boolean = true; +// FIXME we can potentially drop this since the cache implementations we care about turn out to all be stuck 4.6 or earlier +export const DEPRECATE_STRING_ARG_SCHEMAS: boolean = true; +export const DEPRECATE_JSON_API_FALLBACK: boolean = true; +export const DEPRECATE_MODEL_REOPEN: boolean = true; +export const DEPRECATE_EARLY_STATIC: boolean = true; +export const DEPRECATE_HELPERS: boolean = true; +export const DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS: boolean = true; +export const DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE: boolean = true; +export const DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC: boolean = true; +export const DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE: boolean = true; +export const DEPRECATE_A_USAGE: boolean = true; +export const DEPRECATE_PROMISE_PROXIES: boolean = true; +export const DEPRECATE_ARRAY_LIKE: boolean = true; export const DEPRECATE_COMPUTED_CHAINS: boolean = true; +export const DEPRECATE_NON_EXPLICIT_POLYMORPHISM: boolean = true; +export const DEPRECATE_MANY_ARRAY_DUPLICATES: boolean = true; export const DEPRECATE_NON_STRICT_TYPES: boolean = true; export const DEPRECATE_NON_STRICT_ID: boolean = true; -export const DEPRECATE_LEGACY_IMPORTS: boolean = true; export const DEPRECATE_NON_UNIQUE_PAYLOADS: boolean = true; export const DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE: boolean = true; -export const DEPRECATE_MANY_ARRAY_DUPLICATES: boolean = true; export const DEPRECATE_STORE_EXTENDS_EMBER_OBJECT: boolean = true; export const ENABLE_LEGACY_SCHEMA_SERVICE: boolean = true; export const DEPRECATE_EMBER_INFLECTOR: boolean = true; +export const DISABLE_6X_DEPRECATIONS: boolean = true; diff --git a/packages/core-types/package.json b/packages/core-types/package.json index a9be5a044b2..2d8a57b25c1 100644 --- a/packages/core-types/package.json +++ b/packages/core-types/package.json @@ -1,6 +1,6 @@ { "name": "@warp-drive/core-types", - "version": "0.0.0-alpha.107", + "version": "4.12.8", "description": "Provides core logic, utils and types for WarpDrive and EmberData", "keywords": [ "ember-addon" diff --git a/packages/debug/package.json b/packages/debug/package.json index e137e65bbea..3a17493dd4b 100644 --- a/packages/debug/package.json +++ b/packages/debug/package.json @@ -1,6 +1,6 @@ { "name": "@ember-data/debug", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "description": "Provides support for the ember-inspector for apps built with Ember and EmberData", "keywords": [ "ember-addon" diff --git a/packages/diagnostic/package.json b/packages/diagnostic/package.json index 2fcbccc8dfa..02964f7cea4 100644 --- a/packages/diagnostic/package.json +++ b/packages/diagnostic/package.json @@ -1,6 +1,7 @@ { "name": "@warp-drive/diagnostic", - "version": "0.0.0-alpha.107", + "version": "4.12.8", + "private": true, "description": "⚡️ A Lightweight Modern Test Runner", "keywords": [ "test", diff --git a/packages/graph/eslint.config.mjs b/packages/graph/eslint.config.mjs index 63296bac802..c42683457ca 100644 --- a/packages/graph/eslint.config.mjs +++ b/packages/graph/eslint.config.mjs @@ -2,6 +2,7 @@ import { globalIgnores } from '@warp-drive/internal-config/eslint/ignore.js'; import * as node from '@warp-drive/internal-config/eslint/node.js'; import * as typescript from '@warp-drive/internal-config/eslint/typescript.js'; +import { externals } from './vite.config.mjs'; /** @type {import('eslint').Linter.FlatConfig[]} */ export default [ @@ -11,7 +12,7 @@ export default [ // browser (js/ts) ================ typescript.browser({ srcDirs: ['src'], - allowedImports: ['@ember/debug'], + allowedImports: externals, }), // node (module) ================ diff --git a/packages/graph/package.json b/packages/graph/package.json index ad8526a85e8..90e8d04c9d0 100644 --- a/packages/graph/package.json +++ b/packages/graph/package.json @@ -1,6 +1,6 @@ { "name": "@ember-data/graph", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "description": "Provides a normalized graph for managing relationships between resources", "keywords": [ "ember-addon" diff --git a/packages/graph/src/-private/-diff.ts b/packages/graph/src/-private/-diff.ts index 868c4c80510..0041ab66a66 100644 --- a/packages/graph/src/-private/-diff.ts +++ b/packages/graph/src/-private/-diff.ts @@ -1,6 +1,6 @@ import { deprecate } from '@ember/debug'; -import { DEPRECATE_NON_UNIQUE_PAYLOADS } from '@warp-drive/build-config/deprecations'; +import { DEPRECATE_NON_UNIQUE_PAYLOADS, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -178,7 +178,7 @@ export function diffCollection( if (DEBUG) { deprecate( `Expected all entries in the relationship ${relationship.definition.type}:${relationship.definition.key} to be unique, see log for a list of duplicate entry indeces`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-non-unique-relationship-entries', for: 'ember-data', diff --git a/packages/graph/src/-private/-edge-definition.ts b/packages/graph/src/-private/-edge-definition.ts index 6412cbb0400..a6868f487db 100644 --- a/packages/graph/src/-private/-edge-definition.ts +++ b/packages/graph/src/-private/-edge-definition.ts @@ -1,4 +1,5 @@ import type Store from '@ember-data/store'; +import { DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -581,12 +582,27 @@ export function upgradeDefinition( return info; } +type RelationshipDefinition = RelationshipField & { + _inverseKey: (store: Store, modelClass: unknown) => string | null; +}; + +function metaIsRelationshipDefinition(meta: FieldSchema): meta is RelationshipDefinition { + return typeof (meta as RelationshipDefinition)._inverseKey === 'function'; +} + function inverseForRelationship(store: Store, identifier: StableRecordIdentifier | { type: string }, key: string) { const definition = store.schema.fields(identifier).get(key); if (!definition) { return null; } + if (DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE) { + if (metaIsRelationshipDefinition(definition)) { + const modelClass = store.modelFor(identifier.type); + return definition._inverseKey(store, modelClass); + } + } + assert(`Expected ${key} to be a relationship`, isRelationshipField(definition)); assert( `Expected the relationship defintion to specify the inverse type or null.`, diff --git a/packages/graph/src/-private/coerce-id.ts b/packages/graph/src/-private/coerce-id.ts index 7ad7db9c748..4c44a8b22e0 100644 --- a/packages/graph/src/-private/coerce-id.ts +++ b/packages/graph/src/-private/coerce-id.ts @@ -1,6 +1,6 @@ import { deprecate } from '@ember/debug'; -import { DEPRECATE_NON_STRICT_ID } from '@warp-drive/build-config/deprecations'; +import { DEPRECATE_NON_STRICT_ID, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; // Used by the store to normalize IDs entering the store. Despite the fact @@ -24,7 +24,7 @@ export function coerceId(id: Coercable): string | null { `The resource id '<${typeof id}> ${String( id )} ' is not normalized. Update your application code to use '${JSON.stringify(normalized)}' instead.`, - normalized === id, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : normalized === id, { id: 'ember-data:deprecate-non-strict-id', until: '6.0', diff --git a/packages/graph/src/-private/debug/assert-polymorphic-type.ts b/packages/graph/src/-private/debug/assert-polymorphic-type.ts index db9cb188bc6..ee164ad6bb8 100644 --- a/packages/graph/src/-private/debug/assert-polymorphic-type.ts +++ b/packages/graph/src/-private/debug/assert-polymorphic-type.ts @@ -1,11 +1,34 @@ /* eslint-disable @typescript-eslint/no-shadow */ -import type { CacheCapabilitiesManager } from '@ember-data/store/types'; +import type Mixin from '@ember/object/mixin'; + +import type Store from '@ember-data/store'; +import type { CacheCapabilitiesManager, ModelSchema } from '@ember-data/store/types'; +import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import { isLegacyField, isRelationshipField, temporaryConvertToLegacy, type UpgradedMeta } from '../-edge-definition'; +type Model = ModelSchema; + +// A pile of soft-lies to deal with mixin APIs +type ModelWithMixinApis = Model & { + isModel?: boolean; + __isMixin?: boolean; + __mixin: Mixin; + PrototypeMixin: Mixin; + detect: (mixin: Model | Mixin | ModelWithMixinApis) => boolean; + prototype: Model; + [Symbol.hasInstance](model: Model): true; +}; + +function assertModelSchemaIsModel( + schema: ModelSchema | Model | ModelWithMixinApis +): asserts schema is ModelWithMixinApis { + assert(`Expected Schema to be an instance of Model`, 'isModel' in schema && schema.isModel === true); +} + /* Assert that `addedRecord` has a valid type so it can be added to the relationship of the `record`. @@ -27,6 +50,21 @@ let assertPolymorphicType: ( let assertInheritedSchema: (definition: UpgradedMeta, type: string) => void; if (DEBUG) { + const checkPolymorphic = function checkPolymorphic(modelClass: ModelSchema, addedModelClass: ModelSchema) { + assertModelSchemaIsModel(modelClass); + assertModelSchemaIsModel(addedModelClass); + + if (modelClass.__isMixin) { + return ( + modelClass.__mixin.detect(addedModelClass.PrototypeMixin) || + // handle native class extension e.g. `class Post extends Model.extend(Commentable) {}` + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + modelClass.__mixin.detect(Object.getPrototypeOf(addedModelClass).PrototypeMixin) + ); + } + return addedModelClass.prototype instanceof modelClass || modelClass.detect(addedModelClass); + }; + function validateSchema(definition: UpgradedMeta, meta: PrintConfig) { const errors = new Map(); @@ -61,9 +99,9 @@ if (DEBUG) { kind: string; options: { as?: string; - async: boolean; + async?: boolean; polymorphic?: boolean; - inverse: string | null; + inverse?: string | null; }; }; type RelationshipSchemaError = 'name' | 'type' | 'kind' | 'as' | 'async' | 'polymorphic' | 'inverse'; @@ -211,69 +249,143 @@ if (DEBUG) { if (parentDefinition.inverseIsImplicit) { return; } + let asserted = false; + if (parentDefinition.isPolymorphic) { - let meta = store.schema.fields(addedIdentifier).get(parentDefinition.inverseKey); - assert( - `No '${parentDefinition.inverseKey}' field exists on '${ - addedIdentifier.type - }'. To use this type in the polymorphic relationship '${parentDefinition.inverseType}.${ - parentDefinition.key - }' the relationships schema definition for ${addedIdentifier.type} should include:${expectedSchema( - parentDefinition - )}`, - meta - ); - assert( - `Expected the field ${parentDefinition.inverseKey} to be a relationship`, - meta && isRelationshipField(meta) - ); - meta = isLegacyField(meta) ? meta : temporaryConvertToLegacy(meta); + const rawMeta = store.schema.fields(addedIdentifier).get(parentDefinition.inverseKey); assert( - `You should not specify both options.as and options.inverse as null on ${addedIdentifier.type}.${parentDefinition.inverseKey}, as if there is no inverse field there is no abstract type to conform to. You may have intended for this relationship to be polymorphic, or you may have mistakenly set inverse to null.`, - !(meta.options.inverse === null && meta?.options.as?.length) - ); - const errors = validateSchema(parentDefinition, meta); - assert( - `The schema for the relationship '${parentDefinition.inverseKey}' on '${ - addedIdentifier.type - }' type does not correctly implement '${parentDefinition.type}' and thus cannot be assigned to the '${ - parentDefinition.key - }' relationship in '${ - parentIdentifier.type - }'. If using this record in this polymorphic relationship is desired, correct the errors in the schema shown below:${printSchema( - meta, - errors - )}`, - errors.size === 0 + `Expected to find a relationship field schema for ${parentDefinition.inverseKey} on ${addedIdentifier.type} but none was found`, + !rawMeta || isRelationshipField(rawMeta) ); + const meta = rawMeta && (isLegacyField(rawMeta) ? rawMeta : temporaryConvertToLegacy(rawMeta)); + + if (DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { + if (meta?.options?.as) { + asserted = true; + assert( + `No '${parentDefinition.inverseKey}' field exists on '${addedIdentifier.type}'. To use this type in the polymorphic relationship '${parentDefinition.inverseType}.${parentDefinition.key}' the relationships schema definition for ${addedIdentifier.type} should include:${expectedSchema(parentDefinition)}`, + meta + ); + assert( + `You should not specify both options.as and options.inverse as null on ${addedIdentifier.type}.${parentDefinition.inverseKey}, as if there is no inverse field there is no abstract type to conform to. You may have intended for this relationship to be polymorphic, or you may have mistakenly set inverse to null.`, + !(meta.options.inverse === null && meta?.options.as?.length > 0) + ); + const errors = validateSchema(parentDefinition, meta); + assert( + `The schema for the relationship '${parentDefinition.inverseKey}' on '${addedIdentifier.type}' type does not correctly implement '${parentDefinition.type}' and thus cannot be assigned to the '${parentDefinition.key}' relationship in '${parentIdentifier.type}'. If using this record in this polymorphic relationship is desired, correct the errors in the schema shown below:${printSchema(meta, errors)}`, + errors.size === 0 + ); + } + } else { + assert( + `No '${parentDefinition.inverseKey}' field exists on '${ + addedIdentifier.type + }'. To use this type in the polymorphic relationship '${parentDefinition.inverseType}.${ + parentDefinition.key + }' the relationships schema definition for ${addedIdentifier.type} should include:${expectedSchema( + parentDefinition + )}`, + meta + ); + assert( + `Expected the field ${parentDefinition.inverseKey} to be a relationship`, + meta && isRelationshipField(meta) + ); + assert( + `You should not specify both options.as and options.inverse as null on ${addedIdentifier.type}.${parentDefinition.inverseKey}, as if there is no inverse field there is no abstract type to conform to. You may have intended for this relationship to be polymorphic, or you may have mistakenly set inverse to null.`, + !(meta.options.inverse === null && meta?.options.as?.length) + ); + const errors = validateSchema(parentDefinition, meta); + assert( + `The schema for the relationship '${parentDefinition.inverseKey}' on '${ + addedIdentifier.type + }' type does not correctly implement '${parentDefinition.type}' and thus cannot be assigned to the '${ + parentDefinition.key + }' relationship in '${ + parentIdentifier.type + }'. If using this record in this polymorphic relationship is desired, correct the errors in the schema shown below:${printSchema( + meta, + errors + )}`, + errors.size === 0 + ); + } } else if (addedIdentifier.type !== parentDefinition.type) { // if we are not polymorphic // then the addedIdentifier.type must be the same as the parentDefinition.type - let meta = store.schema.fields(addedIdentifier).get(parentDefinition.inverseKey); + const rawMeta = store.schema.fields(addedIdentifier).get(parentDefinition.inverseKey); assert( - `Expected the field ${parentDefinition.inverseKey} to be a relationship`, - !meta || isRelationshipField(meta) + `Expected to find a relationship field schema for ${parentDefinition.inverseKey} on ${addedIdentifier.type} but none was found`, + !rawMeta || isRelationshipField(rawMeta) ); - meta = meta && (isLegacyField(meta) ? meta : temporaryConvertToLegacy(meta)); - if (meta?.options.as === parentDefinition.type) { - // inverse is likely polymorphic but missing the polymorphic flag - let meta = store.schema.fields({ type: parentDefinition.inverseType }).get(parentDefinition.key); - assert(`Expected the field ${parentDefinition.key} to be a relationship`, meta && isRelationshipField(meta)); - meta = isLegacyField(meta) ? meta : temporaryConvertToLegacy(meta); - const errors = validateSchema(definitionWithPolymorphic(inverseDefinition(parentDefinition)), meta); - assert( - `The '<${addedIdentifier.type}>.${ - parentDefinition.inverseKey - }' relationship cannot be used polymorphically because '<${parentDefinition.inverseType}>.${ - parentDefinition.key - } is not a polymorphic relationship. To use this relationship in a polymorphic manner, fix the following schema issues on the relationships schema for '${ - parentDefinition.inverseType - }':${printSchema(meta, errors)}` - ); - } else { + const meta = rawMeta && (isLegacyField(rawMeta) ? rawMeta : temporaryConvertToLegacy(rawMeta)); + + if (!DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { + if (meta?.options.as === parentDefinition.type) { + // inverse is likely polymorphic but missing the polymorphic flag + const inverseMeta = store.schema.fields({ type: parentDefinition.inverseType }).get(parentDefinition.key); + assert( + `Expected to find a relationship field schema for ${parentDefinition.inverseKey} on ${addedIdentifier.type} but none was found`, + inverseMeta && isRelationshipField(inverseMeta) + ); + const legacyInverseMeta = + inverseMeta && (isLegacyField(inverseMeta) ? inverseMeta : temporaryConvertToLegacy(inverseMeta)); + const errors = validateSchema( + definitionWithPolymorphic(inverseDefinition(parentDefinition)), + legacyInverseMeta + ); + assert( + `The '<${addedIdentifier.type}>.${parentDefinition.inverseKey}' relationship cannot be used polymorphically because '<${parentDefinition.inverseType}>.${parentDefinition.key} is not a polymorphic relationship. To use this relationship in a polymorphic manner, fix the following schema issues on the relationships schema for '${parentDefinition.inverseType}':${printSchema(legacyInverseMeta, errors)}` + ); + } else { + assert( + `The '${addedIdentifier.type}' type does not implement '${parentDefinition.type}' and thus cannot be assigned to the '${parentDefinition.key}' relationship in '${parentIdentifier.type}'. If this relationship should be polymorphic, mark ${parentDefinition.inverseType}.${parentDefinition.key} as \`polymorphic: true\` and ${addedIdentifier.type}.${parentDefinition.inverseKey} as implementing it via \`as: '${parentDefinition.type}'\`.` + ); + } + } else if ((meta?.options?.as?.length ?? 0) > 0) { + asserted = true; assert( - `The '${addedIdentifier.type}' type does not implement '${parentDefinition.type}' and thus cannot be assigned to the '${parentDefinition.key}' relationship in '${parentIdentifier.type}'. If this relationship should be polymorphic, mark ${parentDefinition.inverseType}.${parentDefinition.key} as \`polymorphic: true\` and ${addedIdentifier.type}.${parentDefinition.inverseKey} as implementing it via \`as: '${parentDefinition.type}'\`.` + `Expected the field ${parentDefinition.inverseKey} to be a relationship`, + !meta || isRelationshipField(meta) ); + const legacyMeta = meta && (isLegacyField(meta) ? meta : temporaryConvertToLegacy(meta)); + if (legacyMeta?.options.as === parentDefinition.type) { + // inverse is likely polymorphic but missing the polymorphic flag + let meta = store.schema.fields({ type: parentDefinition.inverseType }).get(parentDefinition.key); + assert(`Expected the field ${parentDefinition.key} to be a relationship`, meta && isRelationshipField(meta)); + meta = isLegacyField(meta) ? meta : temporaryConvertToLegacy(meta); + const errors = validateSchema(definitionWithPolymorphic(inverseDefinition(parentDefinition)), meta); + assert( + `The '<${addedIdentifier.type}>.${ + parentDefinition.inverseKey + }' relationship cannot be used polymorphically because '<${parentDefinition.inverseType}>.${ + parentDefinition.key + } is not a polymorphic relationship. To use this relationship in a polymorphic manner, fix the following schema issues on the relationships schema for '${ + parentDefinition.inverseType + }':${printSchema(meta, errors)}` + ); + } else { + assert( + `The '${addedIdentifier.type}' type does not implement '${parentDefinition.type}' and thus cannot be assigned to the '${parentDefinition.key}' relationship in '${parentIdentifier.type}'. If this relationship should be polymorphic, mark ${parentDefinition.inverseType}.${parentDefinition.key} as \`polymorphic: true\` and ${addedIdentifier.type}.${parentDefinition.inverseKey} as implementing it via \`as: '${parentDefinition.type}'\`.` + ); + } + } + } + + if (DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { + if (!asserted) { + const storeService = (store as unknown as { _store: Store })._store; + const addedModelName = addedIdentifier.type; + const parentModelName = parentIdentifier.type; + const key = parentDefinition.key; + const relationshipModelName = parentDefinition.type; + const relationshipClass = storeService.modelFor(relationshipModelName); + const addedClass = storeService.modelFor(addedModelName); + + const assertionMessage = `The '${addedModelName}' type does not implement '${relationshipModelName}' and thus cannot be assigned to the '${key}' relationship in '${parentModelName}'. Make it a descendant of '${relationshipModelName}' or use a mixin of the same name.`; + const isPolymorphic = checkPolymorphic(relationshipClass, addedClass); + + assert(assertionMessage, isPolymorphic); } } }; diff --git a/packages/graph/src/-private/operations/replace-related-record.ts b/packages/graph/src/-private/operations/replace-related-record.ts index 0eae682d6d6..ee8833f2cbb 100644 --- a/packages/graph/src/-private/operations/replace-related-record.ts +++ b/packages/graph/src/-private/operations/replace-related-record.ts @@ -1,6 +1,9 @@ import { deprecate } from '@ember/debug'; -import { DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE } from '@warp-drive/build-config/deprecations'; +import { + DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE, + DISABLE_6X_DEPRECATIONS, +} from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -97,7 +100,7 @@ export default function replaceRelatedRecord(graph: Graph, op: ReplaceRelatedRec } belongsTo relationship but will not be once this deprecation is resolved:\n\n\t${ localState ? 'Added: ' + localState.lid + '\n\t' : '' }${existingState ? 'Removed: ' + existingState.lid : ''}`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-relationship-remote-update-clearing-local-state', for: 'ember-data', @@ -176,7 +179,7 @@ export default function replaceRelatedRecord(graph: Graph, op: ReplaceRelatedRec } belongsTo relationship but will not be once this deprecation is resolved:\n\n\t${ localState ? 'Added: ' + localState.lid + '\n\t' : '' }${existingState ? 'Removed: ' + existingState.lid : ''}`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-relationship-remote-update-clearing-local-state', for: 'ember-data', diff --git a/packages/graph/src/-private/operations/replace-related-records.ts b/packages/graph/src/-private/operations/replace-related-records.ts index 5e04a712411..6cc3c58c6a7 100644 --- a/packages/graph/src/-private/operations/replace-related-records.ts +++ b/packages/graph/src/-private/operations/replace-related-records.ts @@ -1,6 +1,9 @@ import { deprecate } from '@ember/debug'; -import { DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE } from '@warp-drive/build-config/deprecations'; +import { + DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE, + DISABLE_6X_DEPRECATIONS, +} from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -287,7 +290,7 @@ function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOper } hasMany relationship but will not be once this deprecation is resolved by opting into the new behavior:\n\n\tAdded: [${deprecationInfo.additions .map((i) => i.lid) .join(', ')}]\n\tRemoved: [${deprecationInfo.removals.map((i) => i.lid).join(', ')}]`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-relationship-remote-update-clearing-local-state', for: 'ember-data', diff --git a/packages/graph/vite.config.mjs b/packages/graph/vite.config.mjs index 7936563a474..a548ae349de 100644 --- a/packages/graph/vite.config.mjs +++ b/packages/graph/vite.config.mjs @@ -1,6 +1,7 @@ import { createConfig } from '@warp-drive/internal-config/vite/config.js'; export const externals = [ + '@ember/object/mixin', // type only '@ember/debug', // assert, deprecate ]; diff --git a/packages/holodeck/package.json b/packages/holodeck/package.json index 6d5316cd17b..5bfcdf81598 100644 --- a/packages/holodeck/package.json +++ b/packages/holodeck/package.json @@ -1,7 +1,8 @@ { "name": "@warp-drive/holodeck", "description": "⚡️ Simple, Fast HTTP Mocking for Tests", - "version": "0.0.0-alpha.107", + "version": "4.12.8", + "private": true, "license": "MIT", "author": "Chris Thoburn ", "repository": { diff --git a/packages/json-api/package.json b/packages/json-api/package.json index 8409b8167a0..eff6132c4c1 100644 --- a/packages/json-api/package.json +++ b/packages/json-api/package.json @@ -1,6 +1,6 @@ { "name": "@ember-data/json-api", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "description": "Provides a JSON:API document and resource cache implementation for EmberData", "keywords": [ "ember-addon" diff --git a/packages/legacy-compat/package.json b/packages/legacy-compat/package.json index 039c26bf803..14c550af8f3 100644 --- a/packages/legacy-compat/package.json +++ b/packages/legacy-compat/package.json @@ -1,7 +1,7 @@ { "name": "@ember-data/legacy-compat", "description": "Compatibility Shims for Older EmberData", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "license": "MIT", "author": "Chris Thoburn ", "repository": { diff --git a/packages/legacy-compat/src/builders/utils.ts b/packages/legacy-compat/src/builders/utils.ts index 2cc2c71689f..dc29dd7bb26 100644 --- a/packages/legacy-compat/src/builders/utils.ts +++ b/packages/legacy-compat/src/builders/utils.ts @@ -1,7 +1,7 @@ import { deprecate } from '@ember/debug'; import { dasherize } from '@ember-data/request-utils/string'; -import { DEPRECATE_NON_STRICT_TYPES } from '@warp-drive/build-config/deprecations'; +import { DEPRECATE_NON_STRICT_TYPES, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; import type { ResourceIdentifierObject } from '@warp-drive/core-types/spec/json-api-raw'; export function isMaybeIdentifier( @@ -21,7 +21,7 @@ export function normalizeModelName(type: string): string { deprecate( `The resource type '${type}' is not normalized. Update your application code to use '${result}' instead of '${type}'.`, - result === type, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : result === type, { id: 'ember-data:deprecate-non-strict-types', until: '6.0', diff --git a/packages/legacy-compat/src/index.ts b/packages/legacy-compat/src/index.ts index 62a23740ef7..c9077b86e46 100644 --- a/packages/legacy-compat/src/index.ts +++ b/packages/legacy-compat/src/index.ts @@ -1,12 +1,14 @@ import { getOwner } from '@ember/application'; +import { deprecate } from '@ember/debug'; import type Store from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store'; -import { _deprecatingNormalize } from '@ember-data/store/-private'; +import { DEPRECATE_JSON_API_FALLBACK } from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; import type { ObjectValue } from '@warp-drive/core-types/json/raw'; import { FetchManager, upgradeStore } from './-private'; +import { normalizeModelName } from './builders/utils'; import type { AdapterPayload, MinimumAdapterInterface } from './legacy-network-handler/minimum-adapter-interface'; import type { MinimumSerializerInterface, @@ -68,7 +70,7 @@ export function adapterFor(this: Store, modelName: string, _allowMissing?: true) this._adapterCache = this._adapterCache || (Object.create(null) as Record); - const normalizedModelName = _deprecatingNormalize(modelName); + const normalizedModelName = normalizeModelName(modelName); const { _adapterCache } = this; let adapter: (MinimumAdapterInterface & { store: Store }) | undefined = _adapterCache[normalizedModelName]; @@ -93,6 +95,28 @@ export function adapterFor(this: Store, modelName: string, _allowMissing?: true) return adapter; } + if (DEPRECATE_JSON_API_FALLBACK) { + // final fallback, no model specific adapter, no application adapter, no + // `adapter` property on store: use json-api adapter + adapter = _adapterCache['-json-api'] || owner.lookup('adapter:-json-api'); + if (adapter !== undefined) { + deprecate( + `Your application is utilizing a deprecated hidden fallback adapter (-json-api). Please implement an application adapter to function as your fallback.`, + false, + { + id: 'ember-data:deprecate-secret-adapter-fallback', + for: 'ember-data', + until: '5.0', + since: { available: '4.5', enabled: '4.5' }, + } + ); + _adapterCache[normalizedModelName] = adapter; + _adapterCache['-json-api'] = adapter; + + return adapter; + } + } + assert( `No adapter was found for '${modelName}' and no 'application' adapter was found as a fallback.`, _allowMissing @@ -129,7 +153,7 @@ export function serializerFor(this: Store, modelName: string): MinimumSerializer upgradeStore(this); this._serializerCache = this._serializerCache || (Object.create(null) as Record); - const normalizedModelName = _deprecatingNormalize(modelName); + const normalizedModelName = normalizeModelName(modelName); const { _serializerCache } = this; let serializer: (MinimumSerializerInterface & { store: Store }) | undefined = _serializerCache[normalizedModelName]; @@ -190,7 +214,7 @@ export function normalize(this: Store, modelName: string, payload: ObjectValue) `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${typeof modelName}`, typeof modelName === 'string' ); - const normalizedModelName = _deprecatingNormalize(modelName); + const normalizedModelName = normalizeModelName(modelName); const serializer = this.serializerFor(normalizedModelName); const schema = this.modelFor(normalizedModelName); assert( @@ -265,7 +289,7 @@ export function pushPayload(this: Store, modelName: string, inputPayload: Object ); const payload: ObjectValue = inputPayload || (modelName as unknown as ObjectValue); - const normalizedModelName = inputPayload ? _deprecatingNormalize(modelName) : 'application'; + const normalizedModelName = inputPayload ? normalizeModelName(modelName) : 'application'; const serializer = this.serializerFor(normalizedModelName); assert( diff --git a/packages/legacy-compat/src/legacy-network-handler/fetch-manager.ts b/packages/legacy-compat/src/legacy-network-handler/fetch-manager.ts index 1e30751209e..68ae09dd30c 100644 --- a/packages/legacy-compat/src/legacy-network-handler/fetch-manager.ts +++ b/packages/legacy-compat/src/legacy-network-handler/fetch-manager.ts @@ -1,4 +1,4 @@ -import { warn } from '@ember/debug'; +import { deprecate, warn } from '@ember/debug'; import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros'; @@ -13,6 +13,7 @@ import type { } from '@ember-data/store/-private'; import { coerceId } from '@ember-data/store/-private'; import type { FindRecordOptions, ModelSchema } from '@ember-data/store/types'; +import { DEPRECATE_RSVP_PROMISE } from '@warp-drive/build-config/deprecations'; import { DEBUG, TESTING } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import { getOrSetGlobal } from '@warp-drive/core-types/-private'; @@ -28,6 +29,7 @@ import type { AdapterPayload, MinimumAdapterInterface } from './minimum-adapter- import type { MinimumSerializerInterface } from './minimum-serializer-interface'; import { normalizeResponseHelper } from './serializer-response'; import { Snapshot } from './snapshot'; +import { _objectIsAlive } from './utils'; type Deferred = ReturnType>; type AdapterErrors = Error & { errors?: string[]; isAdapterError?: true }; @@ -611,6 +613,7 @@ function _flushPendingSave(store: Store, pending: PendingSaveItem) { const modelName = snapshot.modelName; const modelClass = store.modelFor(modelName); + const record = store._instanceCache.getRecord(identifier); assert(`You tried to update a record but you have no adapter (for ${modelName})`, adapter); assert( @@ -627,6 +630,24 @@ function _flushPendingSave(store: Store, pending: PendingSaveItem) { ); promise = promise.then((adapterPayload) => { + if (!_objectIsAlive(record)) { + if (DEPRECATE_RSVP_PROMISE) { + deprecate( + `A Promise while saving ${modelName} did not resolve by the time your model was destroyed. This will error in a future release.`, + false, + { + id: 'ember-data:rsvp-unresolved-async', + until: '5.0', + for: '@ember-data/store', + since: { + available: '4.5', + enabled: '4.5', + }, + } + ); + } + } + if (adapterPayload) { return normalizeResponseHelper(serializer, store, modelClass, adapterPayload, snapshot.id, operation); } diff --git a/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts b/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts index d1618279415..6864f61fa05 100644 --- a/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts +++ b/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts @@ -1,8 +1,12 @@ +import { deprecate } from '@ember/debug'; + import type Store from '@ember-data/store'; -import type { BaseFinderOptions } from '@ember-data/store/types'; +import type { BaseFinderOptions, ModelSchema } from '@ember-data/store/types'; +import { DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE, DEPRECATE_RSVP_PROMISE } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { StableExistingRecordIdentifier } from '@warp-drive/core-types/identifier'; import type { LegacyRelationshipSchema as RelationshipSchema } from '@warp-drive/core-types/schema/fields'; import type { ExistingResourceObject, JsonApiDocument } from '@warp-drive/core-types/spec/json-api-raw'; @@ -10,6 +14,7 @@ import { upgradeStore } from '../-private'; import { iterateData, payloadIsNotBlank } from './legacy-data-utils'; import type { MinimumAdapterInterface } from './minimum-adapter-interface'; import { normalizeResponseHelper } from './serializer-response'; +import { _bind, _guard, _objectIsAlive, guardDestroyedStore } from './utils'; export function _findHasMany( adapter: MinimumAdapterInterface, @@ -18,9 +23,9 @@ export function _findHasMany( link: string | null | { href: string }, relationship: RelationshipSchema, options: BaseFinderOptions -) { +): Promise { upgradeStore(store); - const promise = Promise.resolve().then(() => { + let promise: Promise = Promise.resolve().then(() => { const snapshot = store._fetchManager.createSnapshot(identifier, options); const useLink = !link || typeof link === 'string'; const relatedLink = useLink ? link : link.href; @@ -35,7 +40,28 @@ export function _findHasMany( return adapter.findHasMany(store, snapshot, relatedLink, relationship); }); - return promise.then((adapterPayload) => { + promise = guardDestroyedStore(promise, store); + promise = promise.then((adapterPayload) => { + const record = store._instanceCache.getRecord(identifier); + + if (!_objectIsAlive(record)) { + if (DEPRECATE_RSVP_PROMISE) { + deprecate( + `A Promise for fetching ${relationship.type} did not resolve by the time your model was destroyed. This will error in a future release.`, + false, + { + id: 'ember-data:rsvp-unresolved-async', + until: '5.0', + for: '@ember-data/store', + since: { + available: '4.5', + enabled: '4.5', + }, + } + ); + } + } + assert( `You made a 'findHasMany' request for a ${identifier.type}'s '${ relationship.name @@ -59,6 +85,14 @@ export function _findHasMany( payload = syncRelationshipDataFromLink(store, payload, identifier as ResourceIdentity, relationship); return store._push(payload, true); }, null); + + if (DEPRECATE_RSVP_PROMISE) { + const record = store._instanceCache.getRecord(identifier); + + promise = _guard(promise, _bind(_objectIsAlive, record)); + } + + return promise as Promise; } export function _findBelongsTo( @@ -69,7 +103,7 @@ export function _findBelongsTo( options: BaseFinderOptions ) { upgradeStore(store); - const promise = Promise.resolve().then(() => { + let promise = Promise.resolve().then(() => { const adapter = store.adapterFor(identifier.type); assert(`You tried to load a belongsTo relationship but you have no adapter (for ${identifier.type})`, adapter); assert( @@ -86,7 +120,33 @@ export function _findBelongsTo( return adapter.findBelongsTo(store, snapshot, relatedLink, relationship); }); + if (DEPRECATE_RSVP_PROMISE) { + const record = store._instanceCache.getRecord(identifier); + promise = guardDestroyedStore(promise, store); + promise = _guard(promise, _bind(_objectIsAlive, record)); + } + return promise.then((adapterPayload) => { + if (DEPRECATE_RSVP_PROMISE) { + const record = store._instanceCache.getRecord(identifier); + + if (!_objectIsAlive(record)) { + deprecate( + `A Promise for fetching ${relationship.type} did not resolve by the time your model was destroyed. This will error in a future release.`, + false, + { + id: 'ember-data:rsvp-unresolved-async', + until: '5.0', + for: '@ember-data/store', + since: { + available: '4.5', + enabled: '4.5', + }, + } + ); + } + } + const modelClass = store.modelFor(relationship.type); const serializer = store.serializerFor(relationship.type); let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findBelongsTo'); @@ -226,11 +286,24 @@ function ensureRelationshipIsSetToParent( } } +type LegacyRelationshipDefinition = { _inverseKey: (store: Store, modelClass: ModelSchema) => string | null }; + +function metaIsRelationshipDefinition(meta: unknown): meta is LegacyRelationshipDefinition { + return typeof meta === 'object' && !!meta && '_inverseKey' in meta && typeof meta._inverseKey === 'function'; +} + function inverseForRelationship(store: Store, identifier: { type: string; id?: string }, key: string) { const definition = store.schema.fields(identifier).get(key); if (!definition) { return null; } + + if (DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE) { + if (metaIsRelationshipDefinition(definition)) { + const modelClass = store.modelFor(identifier.type); + return definition._inverseKey(store, modelClass); + } + } assert( `Expected the field definition to be a relationship`, definition.kind === 'hasMany' || definition.kind === 'belongsTo' diff --git a/packages/legacy-compat/src/legacy-network-handler/snapshot-record-array.ts b/packages/legacy-compat/src/legacy-network-handler/snapshot-record-array.ts index 1121ec58fc8..a6e996c9bd9 100644 --- a/packages/legacy-compat/src/legacy-network-handler/snapshot-record-array.ts +++ b/packages/legacy-compat/src/legacy-network-handler/snapshot-record-array.ts @@ -1,14 +1,19 @@ /** @module @ember-data/legacy-compat */ + +import { deprecate } from '@ember/debug'; + import type Store from '@ember-data/store'; import type { LiveArray } from '@ember-data/store/-private'; import { SOURCE } from '@ember-data/store/-private'; import type { FindAllOptions, ModelSchema } from '@ember-data/store/types'; +import { DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS } from '@warp-drive/build-config/deprecations'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import { upgradeStore } from '../-private'; import type { Snapshot } from './snapshot'; + /** SnapshotRecordArray is not directly instantiable. Instances are provided to consuming application's @@ -180,3 +185,29 @@ export class SnapshotRecordArray { return this._snapshots; } } + +if (DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS) { + /** + The type of the underlying records for the snapshots in the array, as a Model + + @deprecated + @property type + @public + @type {Model} + */ + Object.defineProperty(SnapshotRecordArray.prototype, 'type', { + get() { + deprecate( + `Using SnapshotRecordArray.type to access the ModelClass for a record is deprecated. Use store.modelFor() instead.`, + false, + { + id: 'ember-data:deprecate-snapshot-model-class-access', + until: '5.0', + for: 'ember-data', + since: { available: '4.5.0', enabled: '4.5.0' }, + } + ); + return (this as SnapshotRecordArray)._recordArray.type as ModelSchema; + }, + }); +} diff --git a/packages/legacy-compat/src/legacy-network-handler/snapshot.ts b/packages/legacy-compat/src/legacy-network-handler/snapshot.ts index 1198930fc9a..becb2a7524d 100644 --- a/packages/legacy-compat/src/legacy-network-handler/snapshot.ts +++ b/packages/legacy-compat/src/legacy-network-handler/snapshot.ts @@ -1,11 +1,14 @@ /** @module @ember-data/store */ +import { deprecate } from '@ember/debug'; + import { dependencySatisfies, importSync } from '@embroider/macros'; import type { CollectionEdge, ResourceEdge } from '@ember-data/graph/-private'; import type Store from '@ember-data/store'; -import type { FindRecordOptions } from '@ember-data/store/types'; +import type { FindRecordOptions, ModelSchema } from '@ember-data/store/types'; +import { DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -46,6 +49,16 @@ export class Snapshot { declare adapterOptions?: Record; declare _store: Store; + /** + The type of the underlying record for this snapshot, as a Model. + + @property type + @public + @deprecated + @type {Model} + */ + declare type: ModelSchema; + /** * @method constructor * @constructor @@ -561,3 +574,21 @@ export class Snapshot { return serializer.serialize(this, options); } } + +if (DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS) { + Object.defineProperty(Snapshot.prototype, 'type', { + get(this: Snapshot) { + deprecate( + `Using Snapshot.type to access the ModelClass for a record is deprecated. Use store.modelFor() instead.`, + false, + { + id: 'ember-data:deprecate-snapshot-model-class-access', + until: '5.0', + for: 'ember-data', + since: { available: '4.5.0', enabled: '4.5.0' }, + } + ); + return this._store.modelFor(this.identifier.type); + }, + }); +} diff --git a/packages/legacy-compat/src/legacy-network-handler/utils.ts b/packages/legacy-compat/src/legacy-network-handler/utils.ts new file mode 100644 index 00000000000..69c64fd7e71 --- /dev/null +++ b/packages/legacy-compat/src/legacy-network-handler/utils.ts @@ -0,0 +1,57 @@ +import { deprecate } from '@ember/debug'; + +import type Store from '@ember-data/store'; +import { DEPRECATE_RSVP_PROMISE } from '@warp-drive/build-config/deprecations'; + +function isObject(value: unknown): value is T { + return value !== null && typeof value === 'object'; +} + +export function _objectIsAlive(object: unknown): boolean { + return isObject<{ isDestroyed: boolean; isDestroying: boolean }>(object) + ? !(object.isDestroyed || object.isDestroying) + : false; +} + +export function guardDestroyedStore(promise: Promise, store: Store): Promise { + return promise.then((_v) => { + if (!_objectIsAlive(store)) { + if (DEPRECATE_RSVP_PROMISE) { + deprecate( + `A Promise did not resolve by the time the store was destroyed. This will error in a future release.`, + false, + { + id: 'ember-data:rsvp-unresolved-async', + until: '5.0', + for: '@ember-data/store', + since: { + available: '4.5', + enabled: '4.5', + }, + } + ); + } + } + + return _v; + }); +} + +export function _bind boolean>(fn: T, ...args: unknown[]) { + return function () { + // eslint-disable-next-line prefer-spread + return fn.apply(undefined, args); + }; +} + +export function _guard(promise: Promise, test: () => boolean): Promise { + const guarded = promise.finally(() => { + if (!test()) { + // @ts-expect-error this is a private RSVPPromise API that won't always be there + // eslint-disable-next-line @typescript-eslint/no-unused-expressions, @typescript-eslint/no-unsafe-member-access + guarded._subscribers ? (guarded._subscribers.length = 0) : null; + } + }); + + return guarded; +} diff --git a/packages/model/eslint.config.mjs b/packages/model/eslint.config.mjs index 9ca51cdf5b4..c42683457ca 100644 --- a/packages/model/eslint.config.mjs +++ b/packages/model/eslint.config.mjs @@ -2,6 +2,7 @@ import { globalIgnores } from '@warp-drive/internal-config/eslint/ignore.js'; import * as node from '@warp-drive/internal-config/eslint/node.js'; import * as typescript from '@warp-drive/internal-config/eslint/typescript.js'; +import { externals } from './vite.config.mjs'; /** @type {import('eslint').Linter.FlatConfig[]} */ export default [ @@ -11,17 +12,7 @@ export default [ // browser (js/ts) ================ typescript.browser({ srcDirs: ['src'], - allowedImports: [ - '@ember/array', - '@ember/array/proxy', - '@ember/debug', - '@ember/object/internals', - '@ember/object/proxy', - '@ember/object/computed', - '@ember/object', - '@ember/application', - '@ember/object/promise-proxy-mixin', - ], + allowedImports: externals, }), // node (module) ================ diff --git a/packages/model/package.json b/packages/model/package.json index d1a9b50b9a3..273d3d3159d 100644 --- a/packages/model/package.json +++ b/packages/model/package.json @@ -1,6 +1,6 @@ { "name": "@ember-data/model", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "description": "A basic Ember implementation of a resource presentation layer for use with @ember-data/store", "keywords": [ "ember-addon" diff --git a/packages/model/src/-private/belongs-to.ts b/packages/model/src/-private/belongs-to.ts index 8f3f387cd21..95bbacb4889 100644 --- a/packages/model/src/-private/belongs-to.ts +++ b/packages/model/src/-private/belongs-to.ts @@ -1,6 +1,14 @@ -import { warn } from '@ember/debug'; +import { deprecate, warn } from '@ember/debug'; import { computed } from '@ember/object'; +import { dasherize, singularize } from '@ember-data/request-utils/string'; +import { + DEPRECATE_NON_STRICT_TYPES, + DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC, + DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE, + DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE, + DISABLE_6X_DEPRECATIONS, +} from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { TypeFromInstance } from '@warp-drive/core-types/record'; @@ -8,7 +16,7 @@ import { RecordStore } from '@warp-drive/core-types/symbols'; import { lookupLegacySupport } from './legacy-relationships-support'; import type { MinimalLegacyRecord } from './model-methods'; -import { isElementDescriptor, normalizeModelName } from './util'; +import { isElementDescriptor } from './util'; /** @module @ember-data/model */ @@ -33,22 +41,116 @@ export type NoNull = Exclude; // eslint-disable-next-line @typescript-eslint/no-unused-vars export type RelationshipDecorator = (target: This, key: string, desc?: PropertyDescriptor) => void; // BelongsToDecoratorObject; +function normalizeType(type: string) { + if (DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE) { + if (!type) { + return; + } + } + + if (DEPRECATE_NON_STRICT_TYPES) { + const result = singularize(dasherize(type)); + + deprecate( + `The resource type '${type}' is not normalized. Update your application code to use '${result}' instead of '${type}'.`, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : result === type, + { + id: 'ember-data:deprecate-non-strict-types', + until: '6.0', + for: 'ember-data', + since: { + available: '4.13', + enabled: '5.3', + }, + } + ); + + return result; + } + + return type; +} + function _belongsTo( type: string, options: RelationshipOptions ): RelationshipDecorator { - assert( - `Expected options.async from @belongsTo('${type}', options) to be a boolean`, - options && typeof options.async === 'boolean' - ); - assert( - `Expected options.inverse from @belongsTo('${type}', options) to be either null or the string type of the related resource.`, - options.inverse === null || (typeof options.inverse === 'string' && options.inverse.length > 0) - ); + let opts = options; + let rawType: string | undefined = type; + if (DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE) { + if (typeof type !== 'string' || !type.length) { + deprecate('belongsTo() must specify the string type of the related resource as the first parameter', false, { + id: 'ember-data:deprecate-non-strict-relationships', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + }); + + if (typeof type === 'object') { + opts = type; + rawType = undefined; + } else { + opts = options; + rawType = type; + } + + assert( + 'The first argument to belongsTo must be a string representing a model type key, not an instance of ' + + typeof rawType + + ". E.g., to define a relation to the Person model, use belongsTo('person')", + typeof rawType === 'string' || typeof rawType === 'undefined' + ); + } + } + + if (DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC) { + if (!opts || typeof opts.async !== 'boolean') { + opts = opts || {}; + if (!('async' in opts)) { + // @ts-expect-error the inbound signature is strict to convince the user to use the non-deprecated signature + opts.async = true as Async; + } + deprecate('belongsTo(, ) must specify options.async as either `true` or `false`.', false, { + id: 'ember-data:deprecate-non-strict-relationships', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + }); + } else { + assert(`Expected belongsTo options.async to be a boolean`, opts && typeof opts.async === 'boolean'); + } + } else { + assert(`Expected belongsTo options.async to be a boolean`, opts && typeof opts.async === 'boolean'); + } + + if (DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE) { + if (opts.inverse !== null && (typeof opts.inverse !== 'string' || opts.inverse.length === 0)) { + deprecate( + 'belongsTo(, ) must specify options.inverse as either `null` or the name of the field on the related resource type.', + false, + { + id: 'ember-data:deprecate-non-strict-relationships', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + } + ); + } else { + assert( + `Expected belongsTo options.inverse to be either null or the string type of the related resource.`, + opts.inverse === null || (typeof opts.inverse === 'string' && opts.inverse.length > 0) + ); + } + } else { + assert( + `Expected belongsTo options.inverse to be either null or the string type of the related resource.`, + opts.inverse === null || (typeof opts.inverse === 'string' && opts.inverse.length > 0) + ); + } const meta = { - type: normalizeModelName(type), - options: options, + type: normalizeType(type), + options: opts, kind: 'belongsTo', name: '', }; @@ -285,11 +387,17 @@ export function belongsTo( type?: TypeFromInstance>, options?: RelationshipOptions ): RelationshipDecorator { - if (DEBUG) { + if (!DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE) { assert( - `belongsTo must be invoked with a type and options. Did you mean \`@belongsTo(${type}, { async: false, inverse: null })\`?`, + `belongsTo must be invoked with a type and options. Did you mean \`@belongsTo(, { async: false, inverse: null })\`?`, !isElementDescriptor(arguments as unknown as unknown[]) ); + return _belongsTo(type!, options!); + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return isElementDescriptor(arguments as unknown as any[]) + ? // @ts-expect-error the inbound signature is strict to convince the user to use the non-deprecated signature + (_belongsTo()(...arguments) as RelationshipDecorator) + : _belongsTo(type!, options!); } - return _belongsTo(type!, options!); } diff --git a/packages/model/src/-private/debug/assert-polymorphic-type.ts b/packages/model/src/-private/debug/assert-polymorphic-type.ts index 49c451e0f99..bf9f57b79ee 100644 --- a/packages/model/src/-private/debug/assert-polymorphic-type.ts +++ b/packages/model/src/-private/debug/assert-polymorphic-type.ts @@ -1,8 +1,31 @@ +import type Mixin from '@ember/object/mixin'; + import type { UpgradedMeta } from '@ember-data/graph/-private'; import type Store from '@ember-data/store'; +import type { ModelSchema } from '@ember-data/store/types'; +import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { FieldSchema, LegacyRelationshipSchema } from '@warp-drive/core-types/schema/fields'; + +import { Model } from '../model'; + +// A pile of soft-lies to deal with mixin APIs +type ModelWithMixinApis = Model & { + __isMixin?: boolean; + __mixin: Mixin; + PrototypeMixin: Mixin; + detect: (mixin: Model | Mixin | ModelWithMixinApis) => boolean; + prototype: Model; + [Symbol.hasInstance](model: Model): true; +}; + +function assertModelSchemaIsModel( + schema: ModelSchema | Model | ModelWithMixinApis +): asserts schema is ModelWithMixinApis { + assert(`Expected Schema to be an instance of Model`, schema instanceof Model); +} /* Assert that `addedRecord` has a valid type so it can be added to the @@ -24,6 +47,26 @@ let assertPolymorphicType: ( ) => void; if (DEBUG) { + const checkPolymorphic = function checkPolymorphic(modelClass: ModelSchema, addedModelClass: ModelSchema) { + assertModelSchemaIsModel(modelClass); + assertModelSchemaIsModel(addedModelClass); + + if (modelClass.__isMixin) { + return ( + modelClass.__mixin.detect(addedModelClass.PrototypeMixin) || + // handle native class extension e.g. `class Post extends Model.extend(Commentable) {}` + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + modelClass.__mixin.detect(Object.getPrototypeOf(addedModelClass).PrototypeMixin) + ); + } + + return addedModelClass.prototype instanceof modelClass || modelClass.detect(addedModelClass); + }; + + const isRelationshipField = function isRelationshipField(meta: FieldSchema): meta is LegacyRelationshipSchema { + return meta.kind === 'hasMany' || meta.kind === 'belongsTo'; + }; + // eslint-disable-next-line @typescript-eslint/no-shadow assertPolymorphicType = function assertPolymorphicType( parentIdentifier: StableRecordIdentifier, @@ -34,16 +77,45 @@ if (DEBUG) { if (parentDefinition.inverseIsImplicit) { return; } + let asserted = false; if (parentDefinition.isPolymorphic) { const meta = store.schema.fields(addedIdentifier)?.get(parentDefinition.inverseKey); assert( - `Expected the schema for the field ${parentDefinition.inverseKey} on ${addedIdentifier.type} to be for a legacy relationship`, - !meta || meta.kind === 'belongsTo' || meta.kind === 'hasMany' - ); - assert( - `The schema for the relationship '${parentDefinition.inverseKey}' on '${addedIdentifier.type}' type does not implement '${parentDefinition.type}' and thus cannot be assigned to the '${parentDefinition.key}' relationship in '${parentIdentifier.type}'. The definition should specify 'as: "${parentDefinition.type}"' in options.`, - meta?.options.as === parentDefinition.type + `Expected to find a relationship field schema for ${parentDefinition.inverseKey} on ${addedIdentifier.type} but none was found`, + meta && isRelationshipField(meta) ); + + if (!DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { + assert( + `The schema for the relationship '${parentDefinition.inverseKey}' on '${addedIdentifier.type}' type does not implement '${parentDefinition.type}' and thus cannot be assigned to the '${parentDefinition.key}' relationship in '${parentIdentifier.type}'. The definition should specify 'as: "${parentDefinition.type}"' in options.`, + meta.options.as === parentDefinition.type + ); + } else if ((meta.options.as?.length ?? 0) > 0) { + asserted = true; + assert( + `The schema for the relationship '${parentDefinition.inverseKey}' on '${addedIdentifier.type}' type does not implement '${parentDefinition.type}' and thus cannot be assigned to the '${parentDefinition.key}' relationship in '${parentIdentifier.type}'. The definition should specify 'as: "${parentDefinition.type}"' in options.`, + meta.options.as === parentDefinition.type + ); + } + + if (DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { + if (!asserted) { + store = (store as unknown as { _store: Store })._store + ? (store as unknown as { _store: Store })._store + : store; // allow usage with storeWrapper + const addedModelName = addedIdentifier.type; + const parentModelName = parentIdentifier.type; + const key = parentDefinition.key; + const relationshipModelName = parentDefinition.type; + const relationshipClass = store.modelFor(relationshipModelName); + const addedClass = store.modelFor(addedModelName); + + const assertionMessage = `The '${addedModelName}' type does not implement '${relationshipModelName}' and thus cannot be assigned to the '${key}' relationship in '${parentModelName}'. Make it a descendant of '${relationshipModelName}' or use a mixin of the same name.`; + const isPolymorphic = checkPolymorphic(relationshipClass, addedClass); + + assert(assertionMessage, isPolymorphic); + } + } } }; } diff --git a/packages/model/src/-private/deprecated-promise-proxy.ts b/packages/model/src/-private/deprecated-promise-proxy.ts new file mode 100644 index 00000000000..0adb72058ae --- /dev/null +++ b/packages/model/src/-private/deprecated-promise-proxy.ts @@ -0,0 +1,73 @@ +import { deprecate } from '@ember/debug'; +import { get } from '@ember/object'; + +import { DEBUG } from '@warp-drive/build-config/env'; + +import { PromiseObject } from './promise-proxy-base'; + +function promiseObject(promise: Promise): PromiseObject { + return PromiseObject.create({ promise }) as PromiseObject; +} + +// constructor is accessed in some internals but not including it in the copyright for the deprecation +const ALLOWABLE_METHODS = ['constructor', 'then', 'catch', 'finally']; +const ALLOWABLE_PROPS = ['__ec_yieldable__', '__ec_cancel__']; +const PROXIED_OBJECT_PROPS = ['content', 'isPending', 'isSettled', 'isRejected', 'isFulfilled', 'promise', 'reason']; + +const ProxySymbolString = String(Symbol.for('PROXY_CONTENT')); + +export function deprecatedPromiseObject(promise: Promise): PromiseObject { + const promiseObjectProxy: PromiseObject = promiseObject(promise); + if (!DEBUG) { + return promiseObjectProxy; + } + const handler = { + get(target: object, prop: string, receiver: object): unknown { + if (typeof prop === 'symbol') { + if (String(prop) === ProxySymbolString) { + return; + } + return Reflect.get(target, prop, receiver); + } + + if (prop === 'constructor') { + return target.constructor; + } + + if (ALLOWABLE_PROPS.includes(prop)) { + return target[prop]; + } + + if (!ALLOWABLE_METHODS.includes(prop)) { + deprecate( + `Accessing ${prop} is deprecated. The return type is being changed from PromiseObjectProxy to a Promise. The only available methods to access on this promise are .then, .catch and .finally`, + false, + { + id: 'ember-data:model-save-promise', + until: '5.0', + for: '@ember-data/store', + since: { + available: '4.4', + enabled: '4.4', + }, + } + ); + } else { + return (target[prop] as () => unknown).bind(target); + } + + if (PROXIED_OBJECT_PROPS.includes(prop)) { + return target[prop]; + } + + const value: unknown = get(target, prop); + if (value && typeof value === 'function' && typeof value.bind === 'function') { + return value.bind(receiver); + } + + return undefined; + }, + }; + + return new Proxy(promiseObjectProxy, handler) as PromiseObject; +} diff --git a/packages/model/src/-private/has-many.ts b/packages/model/src/-private/has-many.ts index 9a6fcb286c3..305ae73431c 100644 --- a/packages/model/src/-private/has-many.ts +++ b/packages/model/src/-private/has-many.ts @@ -1,11 +1,17 @@ /** @module @ember-data/model */ -import { deprecate } from '@ember/debug'; +import { deprecate, inspect } from '@ember/debug'; import { computed } from '@ember/object'; import { dasherize, singularize } from '@ember-data/request-utils/string'; -import { DEPRECATE_NON_STRICT_TYPES } from '@warp-drive/build-config/deprecations'; +import { + DEPRECATE_NON_STRICT_TYPES, + DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC, + DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE, + DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE, + DISABLE_6X_DEPRECATIONS, +} from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { TypeFromInstance } from '@warp-drive/core-types/record'; @@ -17,12 +23,18 @@ import type { MinimalLegacyRecord } from './model-methods'; import { isElementDescriptor } from './util'; function normalizeType(type: string) { + if (DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE) { + if (!type) { + return; + } + } + if (DEPRECATE_NON_STRICT_TYPES) { const result = singularize(dasherize(type)); deprecate( `The resource type '${type}' is not normalized. Update your application code to use '${result}' instead of '${type}'.`, - result === type, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : result === type, { id: 'ember-data:deprecate-non-strict-types', until: '6.0', @@ -44,7 +56,66 @@ function _hasMany( type: string, options: RelationshipOptions ): RelationshipDecorator { - assert(`Expected hasMany options.async to be a boolean`, options && typeof options.async === 'boolean'); + if (DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE) { + if (typeof type !== 'string' || !type.length) { + deprecate( + 'hasMany(, ) must specify the string type of the related resource as the first parameter', + false, + { + id: 'ember-data:deprecate-non-strict-relationships', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + } + ); + if (typeof type === 'object') { + options = type; + type = undefined as unknown as string; + } + + assert( + `The first argument to hasMany must be a string representing a model type key, not an instance of ${inspect( + type + )}. E.g., to define a relation to the Comment model, use hasMany('comment')`, + typeof type === 'string' || typeof type === 'undefined' + ); + } + } + + if (DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC) { + if (!options || typeof options.async !== 'boolean') { + options = options || {}; + if (!('async' in options)) { + // @ts-expect-error the inbound signature is strict to convince the user to use the non-deprecated signature + options.async = true; + } + deprecate('hasMany(, ) must specify options.async as either `true` or `false`.', false, { + id: 'ember-data:deprecate-non-strict-relationships', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + }); + } else { + assert(`Expected hasMany options.async to be a boolean`, options && typeof options.async === 'boolean'); + } + } else { + assert(`Expected hasMany options.async to be a boolean`, options && typeof options.async === 'boolean'); + } + + if (DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE) { + if (options.inverse !== null && (typeof options.inverse !== 'string' || options.inverse.length === 0)) { + deprecate( + 'hasMany(, ) must specify options.inverse as either `null` or the name of the field on the related resource type.', + false, + { + id: 'ember-data:deprecate-non-strict-relationships', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + } + ); + } + } // Metadata about relationships is stored on the meta of // the relationship. This is used for introspection and @@ -266,11 +337,17 @@ export function hasMany( type?: TypeFromInstance>, options?: RelationshipOptions ): RelationshipDecorator { - if (DEBUG) { + if (!DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE) { assert( - `hasMany must be invoked with a type and options. Did you mean \`@hasMany(${type}, { async: false, inverse: null })\`?`, + `hasMany must be invoked with a type and options. Did you mean \`@hasMany(, { async: false, inverse: null })\`?`, !isElementDescriptor(arguments as unknown as unknown[]) ); + return _hasMany(type!, options!); + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return isElementDescriptor(arguments as unknown as any[]) + ? // @ts-expect-error the inbound signature is strict to convince the user to use the non-deprecated signature + (_hasMany()(...arguments) as RelationshipDecorator) + : _hasMany(type!, options!); } - return _hasMany(type!, options!); } diff --git a/packages/model/src/-private/legacy-relationships-support.ts b/packages/model/src/-private/legacy-relationships-support.ts index 242529d92e6..ef6fe4831f4 100644 --- a/packages/model/src/-private/legacy-relationships-support.ts +++ b/packages/model/src/-private/legacy-relationships-support.ts @@ -1,3 +1,5 @@ +import { deprecate } from '@ember/debug'; + import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros'; import type { CollectionEdge, Graph, GraphEdge, ResourceEdge, UpgradedMeta } from '@ember-data/graph/-private'; @@ -13,6 +15,7 @@ import { storeFor, } from '@ember-data/store/-private'; import type { BaseFinderOptions } from '@ember-data/store/types'; +import { DEPRECATE_PROMISE_PROXIES } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -717,6 +720,30 @@ function extractIdentifierFromRecord(record: PromiseProxyRecord | OpaqueRecordIn return null; } + if (DEPRECATE_PROMISE_PROXIES) { + if (isPromiseRecord(record)) { + const content = record.content; + assert( + 'You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo or hasMany relationship to the get call.', + content !== undefined + ); + deprecate( + `You passed in a PromiseProxy to a Relationship API that now expects a resolved value. await the value before setting it.`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + since: { + enabled: '4.7', + available: '4.7', + }, + for: 'ember-data', + } + ); + return content ? recordIdentifierFor(content) : null; + } + } + return recordIdentifierFor(record); } @@ -758,3 +785,7 @@ export function areAllInverseRecordsLoaded(store: Store, resource: InnerRelation function isBelongsTo(relationship: GraphEdge): relationship is ResourceEdge { return relationship.definition.kind === 'belongsTo'; } + +function isPromiseRecord(record: PromiseProxyRecord | OpaqueRecordInstance): record is PromiseProxyRecord { + return typeof record === 'object' && !!record && 'then' in record; +} diff --git a/packages/model/src/-private/many-array.ts b/packages/model/src/-private/many-array.ts index c514e4860ce..01c73832264 100644 --- a/packages/model/src/-private/many-array.ts +++ b/packages/model/src/-private/many-array.ts @@ -17,7 +17,11 @@ import { import type { BaseFinderOptions, ModelSchema } from '@ember-data/store/types'; import type { Signal } from '@ember-data/tracking/-private'; import { addToTransaction } from '@ember-data/tracking/-private'; -import { DEPRECATE_MANY_ARRAY_DUPLICATES } from '@warp-drive/build-config/deprecations'; +import { + DEPRECATE_MANY_ARRAY_DUPLICATES, + DEPRECATE_PROMISE_PROXIES, + DISABLE_6X_DEPRECATIONS, +} from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { Cache } from '@warp-drive/core-types/cache'; @@ -317,11 +321,12 @@ export class RelatedCollection extends LiveArray { // dedupe const current = new Set(adds); const unique = Array.from(current); + const uniqueIdentifiers = Array.from(new Set(newValues)); const newArgs = ([start, deleteCount] as unknown[]).concat(unique); const result = Reflect.apply(target[prop], receiver, newArgs) as OpaqueRecordInstance[]; - mutateReplaceRelatedRecords(this, extractIdentifiersFromRecords(unique), _SIGNAL); + mutateReplaceRelatedRecords(this, uniqueIdentifiers, _SIGNAL); return result; } @@ -485,10 +490,39 @@ function extractIdentifiersFromRecords(records: OpaqueRecordInstance[]): StableR } function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | OpaqueRecordInstance) { + if (DEPRECATE_PROMISE_PROXIES) { + if (isPromiseRecord(recordOrPromiseRecord)) { + const content = recordOrPromiseRecord.content; + assert( + 'You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo relationship.', + content !== undefined && content !== null + ); + deprecate( + `You passed in a PromiseProxy to a Relationship API that now expects a resolved value. await the value before setting it.`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + since: { + enabled: '4.7', + available: '4.7', + }, + for: 'ember-data', + } + ); + assertRecordPassedToHasMany(content); + return recordIdentifierFor(content); + } + } + assertRecordPassedToHasMany(recordOrPromiseRecord); return recordIdentifierFor(recordOrPromiseRecord); } +function isPromiseRecord(record: PromiseProxyRecord | OpaqueRecordInstance): record is PromiseProxyRecord { + return Boolean(typeof record === 'object' && record && 'then' in record); +} + function assertNoDuplicates( collection: RelatedCollection, target: StableRecordIdentifier[], @@ -511,7 +545,7 @@ function assertNoDuplicates( .map((r) => (isStableIdentifier(r) ? r.lid : recordIdentifierFor(r).lid)) .sort((a, b) => a.localeCompare(b)) .join('\n\t- ')}`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-many-array-duplicates', for: 'ember-data', diff --git a/packages/model/src/-private/model-methods.ts b/packages/model/src/-private/model-methods.ts index 8af6b3cba97..ed8b690f087 100644 --- a/packages/model/src/-private/model-methods.ts +++ b/packages/model/src/-private/model-methods.ts @@ -5,10 +5,12 @@ import { upgradeStore } from '@ember-data/legacy-compat/-private'; import type Store from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store'; import { peekCache } from '@ember-data/store/-private'; +import { DEPRECATE_SAVE_PROMISE_ACCESS } from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; import type { ChangedAttributesHash } from '@warp-drive/core-types/cache'; import { RecordStore } from '@warp-drive/core-types/symbols'; +import { deprecatedPromiseObject } from './deprecated-promise-proxy'; import type { Errors } from './errors'; import { lookupLegacySupport } from './legacy-relationships-support'; import type RecordState from './record-state'; @@ -88,6 +90,10 @@ export function reload(this: T, options: Record(this: T, options?: Record(this: T, options?: return Promise.resolve(this); } return this.save(options).then((_) => { + // run(() => { this.unloadRecord(); + // }); return this; }); } diff --git a/packages/model/src/-private/model.ts b/packages/model/src/-private/model.ts index c003b428c6a..ea2eff41537 100644 --- a/packages/model/src/-private/model.ts +++ b/packages/model/src/-private/model.ts @@ -2,6 +2,7 @@ @module @ember-data/model */ +import { deprecate, warn } from '@ember/debug'; import EmberObject from '@ember/object'; import type { Snapshot } from '@ember-data/legacy-compat/-private'; @@ -11,6 +12,12 @@ import { recordIdentifierFor, storeFor } from '@ember-data/store'; import { coerceId } from '@ember-data/store/-private'; import { compat } from '@ember-data/tracking'; import { defineSignal } from '@ember-data/tracking/-private'; +import { + DEPRECATE_EARLY_STATIC, + DEPRECATE_MODEL_REOPEN, + DEPRECATE_NON_EXPLICIT_POLYMORPHISM, + DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE, +} from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -38,6 +45,7 @@ import notifyChanges from './notify-changes'; import RecordState, { notifySignal, tagged } from './record-state'; import type BelongsToReference from './references/belongs-to'; import type HasManyReference from './references/has-many'; +import { relationshipFromMeta } from './relationship-meta'; import type { _MaybeBelongsToFields, isSubClass, @@ -1168,22 +1176,49 @@ class Model extends EmberObject implements MinimalLegacyRecord { @param {store} store an instance of Store @return {Model} the type of the relationship, or undefined */ - static typeForRelationship(name: string, store: Store) { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + static typeForRelationship(name: string, store: Store): typeof Model | undefined { + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const relationship = this.relationshipsByName.get(name); + // @ts-expect-error return relationship && store.modelFor(relationship.type); } @computeOnce static get inverseMap(): Record { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } return Object.create(null) as Record; } @@ -1221,10 +1256,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { @return {Object} the inverse relationship, or null */ static inverseFor(name: string, store: Store): LegacyRelationshipSchema | null { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const inverseMap = this.inverseMap; if (inverseMap[name]) { return inverseMap[name]; @@ -1237,10 +1285,27 @@ class Model extends EmberObject implements MinimalLegacyRecord { //Calculate the inverse, ignoring the cache static _findInverseFor(name: string, store: Store): LegacyRelationshipSchema | null { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } + + if (DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { + return legacyFindInverseFor(this, name, store); + } const relationship = this.relationshipsByName.get(name)!; assert(`No relationship named '${name}' on '${this.modelName}' exists.`, relationship); @@ -1322,10 +1387,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { @computeOnce static get relationships(): Map { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const map = new Map(); const relationshipsByName = this.relationshipsByName; @@ -1380,10 +1458,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { */ @computeOnce static get relationshipNames() { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const names: { hasMany: string[]; belongsTo: string[] } = { hasMany: [], belongsTo: [], @@ -1433,10 +1524,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { */ @computeOnce static get relatedTypes(): string[] { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const types: string[] = []; @@ -1496,10 +1600,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { */ @computeOnce static get relationshipsByName(): Map { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const map = new Map(); const rels = this.relationshipsObject; const relationships = Object.keys(rels); @@ -1516,10 +1633,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { @computeOnce static get relationshipsObject(): Record { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const relationships = Object.create(null) as Record; const modelName = this.modelName; @@ -1530,7 +1660,10 @@ class Model extends EmberObject implements MinimalLegacyRecord { // TODO deprecate key being here (meta as unknown as { key: string }).key = name; meta.name = name; - relationships[name] = meta; + const parentModelName = meta.options?.as ?? modelName; + relationships[name] = DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE + ? relationshipFromMeta(meta, parentModelName) + : meta; assert(`Expected options in meta`, meta.options && typeof meta.options === 'object'); assert( @@ -1584,10 +1717,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { */ @computeOnce static get fields(): Map { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const map = new Map(); this.eachComputedProperty((name, meta) => { @@ -1620,10 +1766,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { ) => void, binding?: T ): void { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } this.relationshipsByName.forEach((relationship, name) => { callback.call(binding, name as MaybeRelationshipFields, relationship); @@ -1643,10 +1802,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { @param {any} binding the value to which the callback's `this` should be bound */ static eachRelatedType(callback: (this: T | undefined, type: string) => void, binding?: T) { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const relationshipTypes = this.relatedTypes; @@ -1666,10 +1838,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { knownSide: LegacyRelationshipSchema, store: Store ): 'oneToOne' | 'oneToMany' | 'manyToOne' | 'manyToMany' | 'oneToNone' | 'manyToNone' { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const knownKey = knownSide.name; const knownKind = knownSide.kind; @@ -1730,10 +1915,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { */ @computeOnce static get attributes(): Map { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const map = new Map(); @@ -1795,10 +1993,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { */ @computeOnce static get transformedAttributes() { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } const map = new Map(); @@ -1859,10 +2070,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { callback: (this: T | undefined, key: MaybeAttrFields, attribute: LegacyAttributeField) => void, binding?: T ): void { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } this.attributes.forEach((meta, name) => { callback.call(binding, name as MaybeAttrFields, meta); @@ -1918,10 +2142,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { callback: (this: T | undefined, key: Exclude, type: string) => void, binding?: T ): void { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } this.transformedAttributes.forEach((type: string, name) => { callback.call(binding, name as Exclude, type); @@ -1936,10 +2173,23 @@ class Model extends EmberObject implements MinimalLegacyRecord { @static */ static toString() { - assert( - `Accessing schema information on Models without looking up the model via the store is disallowed.`, - this.modelName - ); + if (DEPRECATE_EARLY_STATIC) { + deprecate( + `Accessing schema information on Models without looking up the model via the store is deprecated. Use store.modelFor (or better Snapshots or the store.getSchemaDefinitionService() apis) instead.`, + Boolean(this.modelName), + { + id: 'ember-data:deprecate-early-static', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + } else { + assert( + `Accessing schema information on Models without looking up the model via the store is disallowed.`, + this.modelName + ); + } return `model:${this.modelName}`; } @@ -2016,8 +2266,40 @@ if (DEBUG) { } }; - delete (Model as unknown as { reopen: unknown }).reopen; - delete (Model as unknown as { reopenClass: unknown }).reopenClass; + if (DEPRECATE_MODEL_REOPEN) { + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalReopen = Model.reopen; + const originalReopenClass = Model.reopenClass; + + // @ts-expect-error Intentional override + Model.reopen = function deprecatedReopen() { + deprecate(`Model.reopen is deprecated. Use Foo extends Model to extend your class instead.`, false, { + id: 'ember-data:deprecate-model-reopen', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + }); + return originalReopen.call(this, ...arguments); + }; + + // @ts-expect-error Intentional override + Model.reopenClass = function deprecatedReopenClass() { + deprecate( + `Model.reopenClass is deprecated. Use Foo extends Model to add static methods and properties to your class instead.`, + false, + { + id: 'ember-data:deprecate-model-reopenclass', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + return originalReopenClass.call(this, ...arguments); + }; + } else { + delete (Model as unknown as { reopen: unknown }).reopen; + delete (Model as unknown as { reopenClass: unknown }).reopenClass; + } } export { Model }; @@ -2030,3 +2312,218 @@ function isRelationshipSchema(meta: unknown): meta is LegacyRelationshipSchema { function isAttributeSchema(meta: unknown): meta is LegacyAttributeField { return typeof meta === 'object' && meta !== null && 'kind' in meta && meta.kind === 'attribute'; } + +function findPossibleInverses( + Klass: typeof Model, + inverseType: typeof Model, + name: string, + relationshipsSoFar?: LegacyRelationshipSchema[] +) { + const possibleRelationships = relationshipsSoFar || []; + + const relationshipMap = inverseType.relationships; + if (!relationshipMap) { + return possibleRelationships; + } + + const relationshipsForType = relationshipMap.get(Klass.modelName); + const relationships = Array.isArray(relationshipsForType) + ? relationshipsForType.filter((relationship) => { + const optionsForRelationship = relationship.options; + + if (!optionsForRelationship.inverse && optionsForRelationship.inverse !== null) { + return true; + } + + return name === optionsForRelationship.inverse; + }) + : null; + + if (relationships) { + // eslint-disable-next-line prefer-spread + possibleRelationships.push.apply(possibleRelationships, relationships); + } + + //Recurse to support polymorphism + if (Klass.superclass) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + findPossibleInverses(Klass.superclass, inverseType, name, possibleRelationships); + } + + return possibleRelationships; +} + +function legacyFindInverseFor(Klass: typeof Model, name: string, store: Store) { + const relationship = Klass.relationshipsByName.get(name); + assert(`No relationship named '${name}' on '${Klass.modelName}' exists.`, relationship); + + const { options } = relationship; + const isPolymorphic = options.polymorphic; + + //If inverse is manually specified to be null, like `comments: hasMany('message', { inverse: null })` + const isExplicitInverseNull = options.inverse === null; + const isAbstractType = !isExplicitInverseNull && isPolymorphic && !store.schema.hasResource(relationship); + + if (isExplicitInverseNull || isAbstractType) { + assert( + `No schema for the abstract type '${relationship.type}' for the polymorphic relationship '${name}' on '${Klass.modelName}' was provided by the SchemaDefinitionService.`, + !isPolymorphic || isExplicitInverseNull + ); + return null; + } + + let fieldOnInverse: string | null | undefined; + let inverseKind: 'belongsTo' | 'hasMany'; + let inverseRelationship: LegacyRelationshipSchema | undefined; + let inverseOptions: LegacyRelationshipSchema['options'] | undefined; + const inverseSchema = Klass.typeForRelationship(name, store); + assert(`No model was found for '${relationship.type}'`, inverseSchema); + + // if the type does not exist and we are not polymorphic + //If inverse is specified manually, return the inverse + if (options.inverse !== undefined) { + fieldOnInverse = options.inverse!; + inverseRelationship = inverseSchema?.relationshipsByName.get(fieldOnInverse); + + assert( + `We found no field named '${fieldOnInverse}' on the schema for '${inverseSchema.modelName}' to be the inverse of the '${name}' relationship on '${Klass.modelName}'. This is most likely due to a missing field on your model definition.`, + inverseRelationship + ); + + // TODO probably just return the whole inverse here + + inverseKind = inverseRelationship.kind; + + inverseOptions = inverseRelationship.options; + } else { + //No inverse was specified manually, we need to use a heuristic to guess one + const parentModelName = relationship.options?.as ?? Klass.modelName; + if (relationship.type === parentModelName) { + warn( + `Detected a reflexive relationship named '${name}' on the schema for '${relationship.type}' without an inverse option. Look at https://guides.emberjs.com/current/models/relationships/#toc_reflexive-relations for how to explicitly specify inverses.`, + false, + { + id: 'ds.model.reflexive-relationship-without-inverse', + } + ); + } + + let possibleRelationships = findPossibleInverses(Klass, inverseSchema, name); + + if (possibleRelationships.length === 0) { + return null; + } + + if (DEBUG) { + const filteredRelationships = possibleRelationships.filter((possibleRelationship) => { + const optionsForRelationship = possibleRelationship.options; + return name === optionsForRelationship.inverse; + }); + + assert( + "You defined the '" + + name + + "' relationship on " + + String(Klass) + + ', but you defined the inverse relationships of type ' + + inverseSchema.toString() + + ' multiple times. Look at https://guides.emberjs.com/current/models/relationships/#toc_explicit-inverses for how to explicitly specify inverses', + filteredRelationships.length < 2 + ); + } + + const explicitRelationship = possibleRelationships.find((rel) => rel.options?.inverse === name); + if (explicitRelationship) { + possibleRelationships = [explicitRelationship]; + } + + assert( + "You defined the '" + + name + + "' relationship on " + + String(Klass) + + ', but multiple possible inverse relationships of type ' + + String(Klass) + + ' were found on ' + + String(inverseSchema) + + '. Look at https://guides.emberjs.com/current/models/relationships/#toc_explicit-inverses for how to explicitly specify inverses', + possibleRelationships.length === 1 + ); + + fieldOnInverse = possibleRelationships[0].name; + inverseKind = possibleRelationships[0].kind; + inverseOptions = possibleRelationships[0].options; + } + + assert(`inverseOptions should be set by now`, inverseOptions); + + // ensure inverse is properly configured + if (DEBUG) { + if (isPolymorphic) { + if (DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { + if (!inverseOptions.as) { + deprecate( + `Relationships that satisfy polymorphic relationships MUST define which abstract-type they are satisfying using 'as'. The field '${fieldOnInverse}' on type '${inverseSchema.modelName}' is misconfigured.`, + false, + { + id: 'ember-data:non-explicit-relationships', + since: { enabled: '4.7', available: '4.7' }, + until: '5.0', + for: 'ember-data', + } + ); + } + } else { + assert( + `Relationships that satisfy polymorphic relationships MUST define which abstract-type they are satisfying using 'as'. The field '${fieldOnInverse}' on type '${inverseSchema.modelName}' is misconfigured.`, + inverseOptions.as + ); + assert( + `options.as should match the expected type of the polymorphic relationship. Expected field '${fieldOnInverse}' on type '${inverseSchema.modelName}' to specify '${relationship.type}' but found '${inverseOptions.as}'`, + !!inverseOptions.as && relationship.type === inverseOptions.as + ); + } + } + } + + // ensure we are properly configured + if (DEBUG) { + if (inverseOptions.polymorphic) { + if (DEPRECATE_NON_EXPLICIT_POLYMORPHISM) { + if (!options.as) { + deprecate( + `Relationships that satisfy polymorphic relationships MUST define which abstract-type they are satisfying using 'as'. The field '${name}' on type '${Klass.modelName}' is misconfigured.`, + false, + { + id: 'ember-data:non-explicit-relationships', + since: { enabled: '4.7', available: '4.7' }, + until: '5.0', + for: 'ember-data', + } + ); + } + } else { + assert( + `Relationships that satisfy polymorphic relationships MUST define which abstract-type they are satisfying using 'as'. The field '${name}' on type '${Klass.modelName}' is misconfigured.`, + options.as + ); + assert( + `options.as should match the expected type of the polymorphic relationship. Expected field '${name}' on type '${Klass.modelName}' to specify '${inverseRelationship!.type}' but found '${options.as}'`, + !!options.as && inverseRelationship!.type === options.as + ); + } + } + } + + assert( + `The ${inverseSchema.modelName}:${fieldOnInverse} relationship declares 'inverse: null', but it was resolved as the inverse for ${Klass.modelName}:${name}.`, + inverseOptions.inverse !== null + ); + + return { + type: inverseSchema.modelName, + name: fieldOnInverse, + kind: inverseKind, + options: inverseOptions, + }; +} diff --git a/packages/model/src/-private/promise-many-array.ts b/packages/model/src/-private/promise-many-array.ts index 8aa0873ac0b..7fe925d9023 100644 --- a/packages/model/src/-private/promise-many-array.ts +++ b/packages/model/src/-private/promise-many-array.ts @@ -1,7 +1,18 @@ +import ArrayMixin, { NativeArray } from '@ember/array'; +import type ArrayProxy from '@ember/array/proxy'; +import { deprecate } from '@ember/debug'; +import Ember from 'ember'; + +import type { CreateRecordProperties } from '@ember-data/store/-private'; import type { BaseFinderOptions } from '@ember-data/store/types'; import { compat } from '@ember-data/tracking'; import { defineSignal } from '@ember-data/tracking/-private'; -import { DEPRECATE_COMPUTED_CHAINS } from '@warp-drive/build-config/deprecations'; +import { + DEPRECATE_A_USAGE, + DEPRECATE_COMPUTED_CHAINS, + DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS, +} from '@warp-drive/build-config/deprecations'; +import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { RelatedCollection as ManyArray } from './many-array'; @@ -31,16 +42,46 @@ export interface HasManyProxyCreateArgs { @class PromiseManyArray @public */ +export interface PromiseManyArray extends Omit, 'destroy' | 'forEach'> { + createRecord(hash: CreateRecordProperties): T; + reload(options: Omit): PromiseManyArray; +} export class PromiseManyArray { declare promise: Promise> | null; declare isDestroyed: boolean; + // @deprecated (isDestroyed is not deprecated) + declare isDestroying: boolean; declare content: ManyArray | null; constructor(promise: Promise>, content?: ManyArray) { this._update(promise, content); this.isDestroyed = false; + this.isDestroying = false; + + if (DEPRECATE_A_USAGE) { + const meta = Ember.meta(this); + meta.hasMixin = (mixin: object) => { + deprecate(`Do not use A() on an EmberData PromiseManyArray`, false, { + id: 'ember-data:no-a-with-array-like', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + }); + if (mixin === NativeArray || mixin === ArrayMixin) { + return true; + } + return false; + }; + } else if (DEBUG) { + const meta = Ember.meta(this); + meta.hasMixin = (mixin: object) => { + assert(`Do not use A() on an EmberData PromiseManyArray`); + }; + } } + //---- Methods/Properties on ArrayProxy that we will keep as our API + /** * Retrieve the length of the content * @property length @@ -156,6 +197,7 @@ export class PromiseManyArray { //---- Methods on EmberObject that we should keep destroy() { + this.isDestroying = true; this.isDestroyed = true; this.content = null; this.promise = null; @@ -217,7 +259,7 @@ if (DEPRECATE_COMPUTED_CHAINS) { return this.content?.length && this.content; }, }; - compat(desc); + compat(PromiseManyArray.prototype, '[]', desc); // ember-source < 3.23 (e.g. 3.20 lts) // requires that the tag `'[]'` be notified @@ -227,6 +269,62 @@ if (DEPRECATE_COMPUTED_CHAINS) { Object.defineProperty(PromiseManyArray.prototype, '[]', desc); } +if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) { + PromiseManyArray.prototype.createRecord = function createRecord( + this: PromiseManyArray, + hash: CreateRecordProperties + ) { + deprecate( + `The createRecord method on ember-data's PromiseManyArray is deprecated. await the promise and work with the ManyArray directly.`, + false, + { + id: 'ember-data:deprecate-promise-many-array-behaviors', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + } + ); + assert('You are trying to createRecord on an async manyArray before it has been created', this.content); + return this.content.createRecord(hash); + }; + + Object.defineProperty(PromiseManyArray.prototype, 'firstObject', { + get() { + deprecate( + `The firstObject property on ember-data's PromiseManyArray is deprecated. await the promise and work with the ManyArray directly.`, + false, + { + id: 'ember-data:deprecate-promise-many-array-behaviors', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + } + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access + return this.content ? this.content.firstObject : undefined; + }, + }); + + Object.defineProperty(PromiseManyArray.prototype, 'lastObject', { + get() { + deprecate( + `The lastObject property on ember-data's PromiseManyArray is deprecated. await the promise and work with the ManyArray directly.`, + false, + { + id: 'ember-data:deprecate-promise-many-array-behaviors', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + } + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access + return this.content ? this.content.lastObject : undefined; + }, + }); +} + function tapPromise(proxy: PromiseManyArray, promise: Promise>) { proxy.isPending = true; proxy.isSettled = false; @@ -249,3 +347,105 @@ function tapPromise(proxy: PromiseManyArray, promise: Promise } ); } + +if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) { + const EmberObjectMethods = [ + 'addObserver', + 'cacheFor', + 'decrementProperty', + 'get', + 'getProperties', + 'incrementProperty', + 'notifyPropertyChange', + 'removeObserver', + 'set', + 'setProperties', + 'toggleProperty', + ]; + EmberObjectMethods.forEach((method) => { + PromiseManyArray.prototype[method] = function delegatedMethod(...args) { + deprecate( + `The ${method} method on ember-data's PromiseManyArray is deprecated. await the promise and work with the ManyArray directly.`, + false, + { + id: 'ember-data:deprecate-promise-many-array-behaviors', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + } + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return Ember[method](this, ...args); + }; + }); + + const InheritedProxyMethods = [ + 'addArrayObserver', + 'addObject', + 'addObjects', + 'any', + 'arrayContentDidChange', + 'arrayContentWillChange', + 'clear', + 'compact', + 'every', + 'filter', + 'filterBy', + 'find', + 'findBy', + 'getEach', + 'includes', + 'indexOf', + 'insertAt', + 'invoke', + 'isAny', + 'isEvery', + 'lastIndexOf', + 'map', + 'mapBy', + // TODO update RFC to note objectAt was deprecated (forEach was left for iteration) + 'objectAt', + 'objectsAt', + 'popObject', + 'pushObject', + 'pushObjects', + 'reduce', + 'reject', + 'rejectBy', + 'removeArrayObserver', + 'removeAt', + 'removeObject', + 'removeObjects', + 'replace', + 'reverseObjects', + 'setEach', + 'setObjects', + 'shiftObject', + 'slice', + 'sortBy', + 'toArray', + 'uniq', + 'uniqBy', + 'unshiftObject', + 'unshiftObjects', + 'without', + ]; + InheritedProxyMethods.forEach((method) => { + PromiseManyArray.prototype[method] = function proxiedMethod(...args) { + deprecate( + `The ${method} method on ember-data's PromiseManyArray is deprecated. await the promise and work with the ManyArray directly.`, + false, + { + id: 'ember-data:deprecate-promise-many-array-behaviors', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + } + ); + assert(`Cannot call ${method} before content is assigned.`, this.content); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return this.content[method](...args); + }; + }); +} diff --git a/packages/model/src/-private/references/belongs-to.ts b/packages/model/src/-private/references/belongs-to.ts index f5363c091f9..c3b227115e5 100644 --- a/packages/model/src/-private/references/belongs-to.ts +++ b/packages/model/src/-private/references/belongs-to.ts @@ -1,8 +1,11 @@ +import { deprecate } from '@ember/debug'; + import type { Graph, ResourceEdge } from '@ember-data/graph/-private'; import type Store from '@ember-data/store'; import type { NotificationType } from '@ember-data/store'; import { cached, compat } from '@ember-data/tracking'; import { defineSignal } from '@ember-data/tracking/-private'; +import { DEPRECATE_PROMISE_PROXIES } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { StableExistingRecordIdentifier } from '@warp-drive/core-types/identifier'; @@ -477,7 +480,32 @@ export default class BelongsToReference< @param {Boolean} [skipFetch] if `true`, do not attempt to fetch unloaded records @return {Promise} */ - async push(doc: SingleResourceDocument, skipFetch?: boolean): Promise { + async push( + maybeDoc: SingleResourceDocument | Promise, + skipFetch?: boolean + ): Promise { + let doc: SingleResourceDocument = maybeDoc as SingleResourceDocument; + if (DEPRECATE_PROMISE_PROXIES) { + if ((maybeDoc as { then: unknown }).then) { + doc = await maybeDoc; + if (doc !== maybeDoc) { + deprecate( + `You passed in a Promise to a Reference API that now expects a resolved value. await the value before setting it.`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + since: { + enabled: '4.7', + available: '4.7', + }, + for: 'ember-data', + } + ); + } + } + } + const { store } = this; const isResourceData = doc.data && isMaybeResource(doc.data); const added = isResourceData diff --git a/packages/model/src/-private/references/has-many.ts b/packages/model/src/-private/references/has-many.ts index 2b13820b149..b41ecfed519 100644 --- a/packages/model/src/-private/references/has-many.ts +++ b/packages/model/src/-private/references/has-many.ts @@ -1,9 +1,12 @@ +import { deprecate } from '@ember/debug'; + import type { CollectionEdge, Graph } from '@ember-data/graph/-private'; import type Store from '@ember-data/store'; import type { NotificationType } from '@ember-data/store'; import type { BaseFinderOptions } from '@ember-data/store/types'; import { cached, compat } from '@ember-data/tracking'; import { defineSignal } from '@ember-data/tracking/-private'; +import { DEPRECATE_PROMISE_PROXIES } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -494,9 +497,31 @@ export default class HasManyReference< @return {Promise} */ async push( - doc: ExistingResourceObject[] | CollectionResourceDocument, + maybeDoc: ExistingResourceObject[] | CollectionResourceDocument, skipFetch?: boolean ): Promise | void> { + let doc = maybeDoc; + if (DEPRECATE_PROMISE_PROXIES) { + if ((maybeDoc as unknown as { then: unknown }).then) { + doc = await (maybeDoc as unknown as Promise); + if (doc !== maybeDoc) { + deprecate( + `You passed in a Promise to a Reference API that now expects a resolved value. await the value before setting it.`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + since: { + enabled: '4.7', + available: '4.7', + }, + for: 'ember-data', + } + ); + } + } + } + const { store } = this; const dataDoc = Array.isArray(doc) ? { data: doc } : doc; const isResourceData = Array.isArray(dataDoc.data) && dataDoc.data.length > 0 && isMaybeResource(dataDoc.data[0]); diff --git a/packages/model/src/-private/relationship-meta.ts b/packages/model/src/-private/relationship-meta.ts new file mode 100644 index 00000000000..e03a181c389 --- /dev/null +++ b/packages/model/src/-private/relationship-meta.ts @@ -0,0 +1,93 @@ +import { dasherize, singularize } from '@ember-data/request-utils/string'; +import type Store from '@ember-data/store'; +import { DEBUG } from '@warp-drive/build-config/env'; +import type { LegacyRelationshipSchema } from '@warp-drive/core-types/schema/fields'; + +import type { Model } from './model'; + +function typeForRelationshipMeta(meta: LegacyRelationshipSchema): string { + let modelName = dasherize(meta.type || meta.name); + + if (meta.kind === 'hasMany') { + modelName = singularize(modelName); + } + + return modelName; +} + +function shouldFindInverse(relationshipMeta: LegacyRelationshipSchema): boolean { + const options = relationshipMeta.options; + return !(options && options.inverse === null); +} + +class RelationshipDefinition { + declare _type: string; + declare __inverseKey: string | null; + declare __hasCalculatedInverse: boolean; + declare parentModelName: string; + declare inverseIsAsync: string | null; + declare meta: LegacyRelationshipSchema; + + constructor(meta: LegacyRelationshipSchema, parentModelName: string) { + this._type = ''; + this.__inverseKey = ''; + this.__hasCalculatedInverse = false; + this.parentModelName = parentModelName; + this.meta = meta; + } + + get kind(): 'belongsTo' | 'hasMany' { + return this.meta.kind; + } + get type(): string { + if (this._type) { + return this._type; + } + this._type = typeForRelationshipMeta(this.meta); + return this._type; + } + get options() { + return this.meta.options; + } + get name(): string { + return this.meta.name; + } + + _inverseKey(store: Store, modelClass: typeof Model): string | null { + if (this.__hasCalculatedInverse === false) { + this._calculateInverse(store, modelClass); + } + return this.__inverseKey; + } + + _calculateInverse(store: Store, modelClass: typeof Model): void { + this.__hasCalculatedInverse = true; + let inverseKey: string | null = null; + let inverse: LegacyRelationshipSchema | null = null; + + if (shouldFindInverse(this.meta)) { + inverse = modelClass.inverseFor(this.name, store); + } + // TODO make this error again for the non-polymorphic case + if (DEBUG) { + if (!this.options.polymorphic) { + modelClass.typeForRelationship(this.name, store); + } + } + + if (inverse) { + inverseKey = inverse.name; + } else { + inverseKey = null; + } + this.__inverseKey = inverseKey; + } +} +export type { RelationshipDefinition }; + +export function relationshipFromMeta( + meta: LegacyRelationshipSchema, + parentModelName: string +): LegacyRelationshipSchema { + return new RelationshipDefinition(meta, parentModelName) as unknown as LegacyRelationshipSchema; +} diff --git a/packages/model/src/-private/schema-provider.ts b/packages/model/src/-private/schema-provider.ts index 88fe9563043..d906b9b29f2 100644 --- a/packages/model/src/-private/schema-provider.ts +++ b/packages/model/src/-private/schema-provider.ts @@ -3,7 +3,11 @@ import { deprecate } from '@ember/debug'; import type Store from '@ember-data/store'; import type { SchemaService } from '@ember-data/store/types'; -import { ENABLE_LEGACY_SCHEMA_SERVICE } from '@warp-drive/build-config/deprecations'; +import { + DEPRECATE_STRING_ARG_SCHEMAS, + DISABLE_6X_DEPRECATIONS, + ENABLE_LEGACY_SCHEMA_SERVICE, +} from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; import type { RecordIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; import type { ObjectValue } from '@warp-drive/core-types/json/raw'; @@ -164,31 +168,60 @@ export class ModelSchemaProvider implements SchemaService { if (ENABLE_LEGACY_SCHEMA_SERVICE) { ModelSchemaProvider.prototype.doesTypeExist = function (type: string): boolean { - deprecate(`Use \`schema.hasResource({ type })\` instead of \`schema.doesTypeExist(type)\``, false, { - id: 'ember-data:schema-service-updates', - until: '6.0', - for: 'ember-data', - since: { - available: '4.13', - enabled: '5.4', - }, - }); + deprecate( + `Use \`schema.hasResource({ type })\` instead of \`schema.doesTypeExist(type)\``, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:schema-service-updates', + until: '6.0', + for: 'ember-data', + since: { + available: '4.13', + enabled: '5.4', + }, + } + ); return this.hasResource({ type }); }; ModelSchemaProvider.prototype.attributesDefinitionFor = function ( resource: RecordIdentifier | { type: string } ): AttributesSchema { - deprecate(`Use \`schema.fields({ type })\` instead of \`schema.attributesDefinitionFor({ type })\``, false, { - id: 'ember-data:schema-service-updates', - until: '6.0', - for: 'ember-data', - since: { - available: '4.13', - enabled: '5.4', - }, - }); - const type = normalizeModelName(resource.type); + let rawType: string; + if (DEPRECATE_STRING_ARG_SCHEMAS) { + if (typeof resource === 'string') { + deprecate( + `relationshipsDefinitionFor expects either a record identifier or an argument of shape { type: string }, received a string.`, + false, + { + id: 'ember-data:deprecate-string-arg-schemas', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.5', available: '4.5' }, + } + ); + rawType = resource; + } else { + rawType = resource.type; + } + } else { + rawType = resource.type; + } + + deprecate( + `Use \`schema.fields({ type })\` instead of \`schema.attributesDefinitionFor({ type })\``, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:schema-service-updates', + until: '6.0', + for: 'ember-data', + since: { + available: '4.13', + enabled: '5.4', + }, + } + ); + const type = normalizeModelName(rawType); if (!this._schemas.has(type)) { this._loadModelSchema(type); @@ -200,16 +233,41 @@ if (ENABLE_LEGACY_SCHEMA_SERVICE) { ModelSchemaProvider.prototype.relationshipsDefinitionFor = function ( resource: RecordIdentifier | { type: string } ): RelationshipsSchema { - deprecate(`Use \`schema.fields({ type })\` instead of \`schema.relationshipsDefinitionFor({ type })\``, false, { - id: 'ember-data:schema-service-updates', - until: '6.0', - for: 'ember-data', - since: { - available: '4.13', - enabled: '5.4', - }, - }); - const type = normalizeModelName(resource.type); + let rawType: string; + if (DEPRECATE_STRING_ARG_SCHEMAS) { + if (typeof resource === 'string') { + deprecate( + `relationshipsDefinitionFor expects either a record identifier or an argument of shape { type: string }, received a string.`, + false, + { + id: 'ember-data:deprecate-string-arg-schemas', + for: 'ember-data', + until: '5.0', + since: { enabled: '4.5', available: '4.5' }, + } + ); + rawType = resource; + } else { + rawType = resource.type; + } + } else { + rawType = resource.type; + } + + deprecate( + `Use \`schema.fields({ type })\` instead of \`schema.relationshipsDefinitionFor({ type })\``, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:schema-service-updates', + until: '6.0', + for: 'ember-data', + since: { + available: '4.13', + enabled: '5.4', + }, + } + ); + const type = normalizeModelName(rawType); if (!this._schemas.has(type)) { this._loadModelSchema(type); diff --git a/packages/model/src/-private/util.ts b/packages/model/src/-private/util.ts index b6f924f360d..b9f0380dde3 100644 --- a/packages/model/src/-private/util.ts +++ b/packages/model/src/-private/util.ts @@ -1,7 +1,7 @@ import { deprecate } from '@ember/debug'; import { dasherize } from '@ember-data/request-utils/string'; -import { DEPRECATE_NON_STRICT_TYPES } from '@warp-drive/build-config/deprecations'; +import { DEPRECATE_NON_STRICT_TYPES, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; export type DecoratorPropertyDescriptor = (PropertyDescriptor & { initializer?: () => unknown }) | undefined; @@ -31,7 +31,7 @@ export function normalizeModelName(type: string): string { deprecate( `The resource type '${type}' is not normalized. Update your application code to use '${result}' instead of '${type}'.`, - result === type, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : result === type, { id: 'ember-data:deprecate-non-strict-types', until: '6.0', diff --git a/packages/model/vite.config.mjs b/packages/model/vite.config.mjs index 0bd74020510..c06cb9e5c61 100644 --- a/packages/model/vite.config.mjs +++ b/packages/model/vite.config.mjs @@ -1,6 +1,7 @@ import { createConfig } from '@warp-drive/internal-config/vite/config.js'; export const externals = [ + 'ember', '@ember/service', '@ember/debug', '@ember/object/computed', diff --git a/packages/request-utils/package.json b/packages/request-utils/package.json index 2ff1dc08010..eb27c28be46 100644 --- a/packages/request-utils/package.json +++ b/packages/request-utils/package.json @@ -1,7 +1,7 @@ { "name": "@ember-data/request-utils", "description": "Request Building Utilities for use with EmberData", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": false, "license": "MIT", "author": "Chris Thoburn ", diff --git a/packages/request-utils/src/deprecation-support.ts b/packages/request-utils/src/deprecation-support.ts index 15a4d9ab05c..d0229dc3906 100644 --- a/packages/request-utils/src/deprecation-support.ts +++ b/packages/request-utils/src/deprecation-support.ts @@ -2,7 +2,7 @@ import { deprecate } from '@ember/debug'; import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros'; -import { DEPRECATE_EMBER_INFLECTOR } from '@warp-drive/build-config/deprecations'; +import { DEPRECATE_EMBER_INFLECTOR, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; import { defaultRules as WarpDriveDefaults } from './-private/string/inflections'; import { irregular, plural, singular, uncountable } from './string'; @@ -85,7 +85,7 @@ if (DEPRECATE_EMBER_INFLECTOR) { deprecate( `WarpDrive/EmberData no longer uses ember-inflector for pluralization.\nPlease \`import { plural } from '@ember-data/request-utils/string';\` instead to register a custom pluralization rule for use with EmberData.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'warp-drive.ember-inflector', until: '6.0.0', @@ -109,7 +109,7 @@ if (DEPRECATE_EMBER_INFLECTOR) { deprecate( `WarpDrive/EmberData no longer uses ember-inflector for singularization.\nPlease \`import { singular } from '@ember-data/request-utils/string';\` instead to register a custom singularization rule for use with EmberData.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'warp-drive.ember-inflector', until: '6.0.0', @@ -141,7 +141,7 @@ if (DEPRECATE_EMBER_INFLECTOR) { deprecate( `WarpDrive/EmberData no longer uses ember-inflector for irregular rules.\nPlease \`import { irregular } from '@ember-data/request-utils/string';\` instead to register a custom irregular rule for use with EmberData for '${actualSingle}' <=> '${plur}'.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'warp-drive.ember-inflector', until: '6.0.0', @@ -165,7 +165,7 @@ if (DEPRECATE_EMBER_INFLECTOR) { deprecate( `WarpDrive/EmberData no longer uses ember-inflector for uncountable rules.\nPlease \`import { uncountable } from '@ember-data/request-utils/string';\` instead to register a custom uncountable rule for '${word}' for use with EmberData.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'warp-drive.ember-inflector', until: '6.0.0', @@ -184,7 +184,7 @@ if (DEPRECATE_EMBER_INFLECTOR) { deprecate( `WarpDrive/EmberData no longer uses ember-inflector for pluralization.\nPlease \`import { plural } from '@ember-data/request-utils/string';\` instead to register a custom pluralization rule for use with EmberData.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'warp-drive.ember-inflector', until: '6.0.0', @@ -205,7 +205,7 @@ if (DEPRECATE_EMBER_INFLECTOR) { deprecate( `WarpDrive/EmberData no longer uses ember-inflector for singularization.\nPlease \`import { singular } from '@ember-data/request-utils/string';\` instead to register a custom singularization rule for use with EmberData.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'warp-drive.ember-inflector', until: '6.0.0', @@ -226,7 +226,7 @@ if (DEPRECATE_EMBER_INFLECTOR) { deprecate( `WarpDrive/EmberData no longer uses ember-inflector for irregular rules.\nPlease \`import { irregular } from '@ember-data/request-utils/string';\` instead to register a custom irregular rule for use with EmberData.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'warp-drive.ember-inflector', until: '6.0.0', @@ -247,7 +247,7 @@ if (DEPRECATE_EMBER_INFLECTOR) { deprecate( `WarpDrive/EmberData no longer uses ember-inflector for uncountable rules.\nPlease \`import { uncountable } from '@ember-data/request-utils/string';\` instead to register a custom uncountable rule for use with EmberData.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'warp-drive.ember-inflector', until: '6.0.0', diff --git a/packages/request-utils/src/index.ts b/packages/request-utils/src/index.ts index 337195d6c4e..6a52199e6b0 100644 --- a/packages/request-utils/src/index.ts +++ b/packages/request-utils/src/index.ts @@ -1,5 +1,6 @@ import { deprecate } from '@ember/debug'; +import { DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { Cache } from '@warp-drive/core-types/cache'; @@ -753,7 +754,7 @@ export class CachePolicy { const _config = arguments.length === 1 ? config : (arguments[1] as unknown as PolicyConfig); deprecate( `Passing a Store to the CachePolicy is deprecated, please pass only a config instead.`, - arguments.length === 1, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : arguments.length === 1, { id: 'ember-data:request-utils:lifetimes-service-store-arg', since: { @@ -934,7 +935,7 @@ export class LifetimesService extends CachePolicy { constructor(config: PolicyConfig) { deprecate( `\`import { LifetimesService } from '@ember-data/request-utils';\` is deprecated, please use \`import { CachePolicy } from '@ember-data/request-utils';\` instead.`, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-lifetimes-service-import', since: { diff --git a/packages/request/package.json b/packages/request/package.json index 595810bb474..d53beac17cd 100644 --- a/packages/request/package.json +++ b/packages/request/package.json @@ -1,7 +1,7 @@ { "name": "@ember-data/request", "description": "⚡️ A simple, small and fast framework-agnostic library to make `fetch` happen", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "license": "MIT", "author": "Chris Thoburn ", "repository": { diff --git a/packages/rest/package.json b/packages/rest/package.json index 33e757cb3b0..2815c20498b 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,7 +1,7 @@ { "name": "@ember-data/rest", "description": "REST Format Support for EmberData", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": false, "license": "MIT", "author": "Chris Thoburn ", diff --git a/packages/serializer/package.json b/packages/serializer/package.json index 143a96af484..00d97de292d 100644 --- a/packages/serializer/package.json +++ b/packages/serializer/package.json @@ -1,6 +1,6 @@ { "name": "@ember-data/serializer", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "description": "Provides Legacy JSON, JSON:API and REST Implementations of the Serializer Interface for use with @ember-data/store", "keywords": [ "ember-addon" diff --git a/packages/store/package.json b/packages/store/package.json index 1280d1236e4..0898da76cd9 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -1,6 +1,6 @@ { "name": "@ember-data/store", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "description": "The core of EmberData. Provides the Store service which coordinates the cache with the network and presentation layers.", "keywords": [ "ember-addon" diff --git a/packages/store/src/-private.ts b/packages/store/src/-private.ts index 532b54b8e04..9100d7b2a94 100644 --- a/packages/store/src/-private.ts +++ b/packages/store/src/-private.ts @@ -1,6 +1,11 @@ /** @module @ember-data/store */ +import { assert, deprecate } from '@ember/debug'; + +import { DEPRECATE_HELPERS } from '@warp-drive/build-config/deprecations'; + +import { normalizeModelName as _normalize } from './-private/utils/normalize-model-name'; export { Store, storeFor } from './-private/store-service'; @@ -47,5 +52,33 @@ export { peekCache, removeRecordDataFor } from './-private/caches/cache-utils'; // @ember-data/model needs these temporarily export { setRecordIdentifier, StoreMap } from './-private/caches/instance-cache'; export { setCacheFor } from './-private/caches/cache-utils'; -export { normalizeModelName as _deprecatingNormalize } from './-private/utils/normalize-model-name'; export type { StoreRequestInput } from './-private/cache-handler/handler'; + +/** + This method normalizes a modelName into the format EmberData uses + internally by dasherizing it. + + @method normalizeModelName + @static + @public + @deprecated + @for @ember-data/store + @param {String} modelName + @return {String} normalizedModelName +*/ +export function normalizeModelName(modelName: string) { + if (DEPRECATE_HELPERS) { + deprecate( + `the helper function normalizeModelName is deprecated. You should use model names that are already normalized, or use string helpers of your own. This function is primarily an alias for dasherize from @ember/string.`, + false, + { + id: 'ember-data:deprecate-normalize-modelname-helper', + for: 'ember-data', + until: '5.0', + since: { available: '4.7', enabled: '4.7' }, + } + ); + return _normalize(modelName); + } + assert(`normalizeModelName support has been removed`); +} diff --git a/packages/store/src/-private/proxies/promise-proxies.ts b/packages/store/src/-private/proxies/promise-proxies.ts new file mode 100644 index 00000000000..bc2d0f69b10 --- /dev/null +++ b/packages/store/src/-private/proxies/promise-proxies.ts @@ -0,0 +1,225 @@ +import { deprecate } from '@ember/debug'; +import { get } from '@ember/object'; + +import { DEBUG } from '@warp-drive/build-config/env'; + +import type IdentifierArray from '../record-arrays/identifier-array'; +import { PromiseArrayProxy, PromiseObjectProxy } from './promise-proxy-base'; + +/** + @module @ember-data/store +*/ + +/** + A `PromiseArray` is an object that acts like both an `Ember.Array` + and a promise. When the promise is resolved the resulting value + will be set to the `PromiseArray`'s `content` property. This makes + it easy to create data bindings with the `PromiseArray` that will be + updated when the promise resolves. + + This class should not be imported and instantiated directly. + + For more information see the [Ember.PromiseProxyMixin + documentation](/ember/release/classes/PromiseProxyMixin). + + Example + + ```javascript + let promiseArray = PromiseArray.create({ + promise: $.getJSON('/some/remote/data.json') + }); + + promiseArray.length; // 0 + + promiseArray.then(function() { + promiseArray.length; // 100 + }); + ``` + + @class PromiseArray + @public + @extends Ember.ArrayProxy + @uses Ember.PromiseProxyMixin +*/ + +/** + A `PromiseObject` is an object that acts like both an `EmberObject` + and a promise. When the promise is resolved, then the resulting value + will be set to the `PromiseObject`'s `content` property. This makes + it easy to create data bindings with the `PromiseObject` that will + be updated when the promise resolves. + + This class should not be imported and instantiated directly. + + For more information see the [Ember.PromiseProxyMixin + documentation](/ember/release/classes/PromiseProxyMixin.html). + + Example + + ```javascript + let promiseObject = PromiseObject.create({ + promise: $.getJSON('/some/remote/data.json') + }); + + promiseObject.name; // null + + promiseObject.then(function() { + promiseObject.name; // 'Tomster' + }); + ``` + + @class PromiseObject + @public + @extends Ember.ObjectProxy + @uses Ember.PromiseProxyMixin +*/ +export { PromiseObjectProxy as PromiseObject }; + +function _promiseObject(promise: Promise): Promise { + return PromiseObjectProxy.create({ promise }) as Promise; +} + +function _promiseArray(promise: Promise>): Promise> { + // @ts-expect-error this bucket of lies allows us to avoid typing the promise proxy which would + // require us to override a lot of Ember's types. + return PromiseArrayProxy.create({ promise }) as unknown as Promise>; +} + +// constructor is accessed in some internals but not including it in the copyright for the deprecation +const ALLOWABLE_METHODS = ['constructor', 'then', 'catch', 'finally']; +const ALLOWABLE_PROPS = ['__ec_yieldable__', '__ec_cancel__'] as const; +const PROXIED_ARRAY_PROPS = [ + 'length', + '[]', + 'firstObject', + 'lastObject', + 'meta', + 'content', + 'isPending', + 'isSettled', + 'isRejected', + 'isFulfilled', + 'promise', + 'reason', +]; +const PROXIED_OBJECT_PROPS = ['content', 'isPending', 'isSettled', 'isRejected', 'isFulfilled', 'promise', 'reason']; + +function isAllowedProp(prop: string): prop is (typeof ALLOWABLE_PROPS)[number] { + return ALLOWABLE_PROPS.includes(prop as (typeof ALLOWABLE_PROPS)[number]); +} + +type SensitiveArray = { + __ec_yieldable__: unknown; + __ec_cancel__: unknown; +} & Promise>; + +type SensitiveObject = { + __ec_yieldable__: unknown; + __ec_cancel__: unknown; +} & Promise; + +export function promiseArray(promise: Promise>): Promise> { + const promiseObjectProxy = _promiseArray(promise); + if (!DEBUG) { + return promiseObjectProxy; + } + const handler = { + get(target: SensitiveArray, prop: string, receiver: object): unknown { + if (typeof prop === 'symbol') { + return Reflect.get(target, prop, receiver); + } + if (isAllowedProp(prop)) { + return target[prop]; + } + if (!ALLOWABLE_METHODS.includes(prop)) { + deprecate( + `Accessing ${prop} on this PromiseArray is deprecated. The return type is being changed from PromiseArray to a Promise. The only available methods to access on this promise are .then, .catch and .finally`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + for: '@ember-data/store', + since: { + available: '4.7', + enabled: '4.7', + }, + } + ); + } + + // @ts-expect-error difficult to coerce target to the classic ember proxy + const value: unknown = target[prop]; + if (value && typeof value === 'function' && typeof value.bind === 'function') { + return value.bind(target); + } + + if (PROXIED_ARRAY_PROPS.includes(prop)) { + return value; + } + + return undefined; + }, + }; + + return new Proxy(promiseObjectProxy, handler); +} + +const ProxySymbolString = String(Symbol.for('PROXY_CONTENT')); + +export function promiseObject(promise: Promise): Promise { + const promiseObjectProxy = _promiseObject(promise); + if (!DEBUG) { + return promiseObjectProxy; + } + const handler = { + get(target: SensitiveObject, prop: string, receiver: object): unknown { + if (typeof prop === 'symbol') { + if (String(prop) === ProxySymbolString) { + return; + } + return Reflect.get(target, prop, receiver); + } + + if (prop === 'constructor') { + return target.constructor; + } + + if (isAllowedProp(prop)) { + return target[prop]; + } + + if (!ALLOWABLE_METHODS.includes(prop)) { + deprecate( + `Accessing ${prop} on this PromiseObject is deprecated. The return type is being changed from PromiseObject to a Promise. The only available methods to access on this promise are .then, .catch and .finally`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + for: '@ember-data/store', + since: { + available: '4.7', + enabled: '4.7', + }, + } + ); + } else { + // @ts-expect-error difficult to coerce target to the classic ember proxy + return (target[prop] as () => unknown).bind(target); + } + + if (PROXIED_OBJECT_PROPS.includes(prop)) { + // @ts-expect-error difficult to coerce target to the classic ember proxy + return target[prop]; + } + + const value: unknown = get(target, prop); + if (value && typeof value === 'function' && typeof value.bind === 'function') { + return value.bind(receiver); + } + + return undefined; + }, + }; + + return new Proxy(promiseObjectProxy, handler); +} diff --git a/packages/store/src/-private/proxies/promise-proxy-base.d.ts b/packages/store/src/-private/proxies/promise-proxy-base.d.ts new file mode 100644 index 00000000000..8c54a04b1de --- /dev/null +++ b/packages/store/src/-private/proxies/promise-proxy-base.d.ts @@ -0,0 +1,65 @@ +import ArrayProxy from '@ember/array/proxy'; +import ObjectProxy from '@ember/object/proxy'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export interface PromiseArrayProxy extends ArrayProxy, Promise {} +export class PromiseArrayProxy extends ArrayProxy { + declare content: T; + + /* + * If the proxied promise is rejected this will contain the reason + * provided. + */ + declare reason: string | Error; + /* + * Once the proxied promise has settled this will become `false`. + */ + declare isPending: boolean; + /* + * Once the proxied promise has settled this will become `true`. + */ + declare isSettled: boolean; + /* + * Will become `true` if the proxied promise is rejected. + */ + declare isRejected: boolean; + /* + * Will become `true` if the proxied promise is fulfilled. + */ + declare isFulfilled: boolean; + /* + * The promise whose fulfillment value is being proxied by this object. + */ + declare promise: Promise; +} + +export interface PromiseObjectProxy extends ObjectProxy, Promise {} +export class PromiseObjectProxy extends ObjectProxy { + declare content?: T | null; + + /* + * If the proxied promise is rejected this will contain the reason + * provided. + */ + reason: string | Error; + /* + * Once the proxied promise has settled this will become `false`. + */ + isPending: boolean; + /* + * Once the proxied promise has settled this will become `true`. + */ + isSettled: boolean; + /* + * Will become `true` if the proxied promise is rejected. + */ + isRejected: boolean; + /* + * Will become `true` if the proxied promise is fulfilled. + */ + isFulfilled: boolean; + /* + * The promise whose fulfillment value is being proxied by this object. + */ + promise: Promise; +} diff --git a/packages/store/src/-private/proxies/promise-proxy-base.js b/packages/store/src/-private/proxies/promise-proxy-base.js new file mode 100644 index 00000000000..87c98734d04 --- /dev/null +++ b/packages/store/src/-private/proxies/promise-proxy-base.js @@ -0,0 +1,9 @@ +import ArrayProxy from '@ember/array/proxy'; +import { reads } from '@ember/object/computed'; +import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; +import ObjectProxy from '@ember/object/proxy'; + +export class PromiseArrayProxy extends ArrayProxy.extend(PromiseProxyMixin) { + @reads('content.meta') meta; +} +export const PromiseObjectProxy = ObjectProxy.extend(PromiseProxyMixin); diff --git a/packages/store/src/-private/record-arrays/identifier-array.ts b/packages/store/src/-private/record-arrays/identifier-array.ts index d884f92f0ec..ae693334122 100644 --- a/packages/store/src/-private/record-arrays/identifier-array.ts +++ b/packages/store/src/-private/record-arrays/identifier-array.ts @@ -1,6 +1,11 @@ /** @module @ember-data/store */ +import { deprecate } from '@ember/debug'; +import { get, set } from '@ember/object'; +import { compare } from '@ember/utils'; +import Ember from 'ember'; + import { compat } from '@ember-data/tracking'; import type { Signal } from '@ember-data/tracking/-private'; import { @@ -10,6 +15,14 @@ import { defineSignal, subscribe, } from '@ember-data/tracking/-private'; +import { + DEPRECATE_A_USAGE, + DEPRECATE_ARRAY_LIKE, + DEPRECATE_COMPUTED_CHAINS, + DEPRECATE_PROMISE_PROXIES, + DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS, +} from '@warp-drive/build-config/deprecations'; +import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import { getOrSetGlobal } from '@warp-drive/core-types/-private'; import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier'; @@ -21,6 +34,7 @@ import type { OpaqueRecordInstance } from '../../-types/q/record-instance'; import { isStableIdentifier } from '../caches/identifier-cache'; import { recordIdentifierFor } from '../caches/instance-cache'; import type { RecordArrayManager } from '../managers/record-array-manager'; +import { promiseArray } from '../proxies/promise-proxies'; import type { Store } from '../store-service'; import { NativeProxy } from './native-proxy-type-fix'; @@ -93,6 +107,19 @@ export type IdentifierArrayCreateOptions = { meta?: Record | null; }; +function deprecateArrayLike(className: string, fnName: string, replName: string) { + deprecate( + `The \`${fnName}\` method on the class ${className} is deprecated. Use the native array method \`${replName}\` instead.`, + false, + { + id: 'ember-data:deprecate-array-like', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + } + ); +} + interface PrivateState { links: Links | PaginationLinks | null; meta: Record | null; @@ -182,6 +209,16 @@ export class IdentifierArray { declare links: Links | PaginationLinks | null; declare meta: Record | null; declare modelName?: TypeFromInstanceOrString; + + /** + The modelClass represented by this record array. + + @property type + @public + @deprecated + @type {subclass of Model} + */ + declare type: unknown; /** The store that created this record array. @@ -299,6 +336,7 @@ export class IdentifierArray { } const args: unknown[] = Array.prototype.slice.call(arguments); assert(`Cannot start a new array transaction while a previous transaction is underway`, !transaction); + transaction = true; const result = self[MUTATE]!(target, receiver, prop as string, args, _SIGNAL); transaction = false; @@ -312,6 +350,18 @@ export class IdentifierArray { } if (isSelfProp(self, prop)) { + if (DEPRECATE_ARRAY_LIKE) { + if (prop === 'firstObject') { + deprecateArrayLike(self.DEPRECATED_CLASS_NAME, prop as string, '[0]'); + // @ts-expect-error adding MutableArray method calling index signature + return receiver[0]; + } else if (prop === 'lastObject') { + deprecateArrayLike(self.DEPRECATED_CLASS_NAME, prop as string, 'at(-1)'); + // @ts-expect-error adding MutableArray method calling index signature + return receiver[receiver.length - 1]; + } + } + if (prop === NOTIFY || prop === ARRAY_SIGNAL || prop === SOURCE) { return self[prop]; } @@ -396,6 +446,7 @@ export class IdentifierArray { const original: StableRecordIdentifier | undefined = target[index]; const newIdentifier = extractIdentifierFromRecord(value); + // FIXME this line was added on main and I'm not sure why (target as unknown as Record)[index] = newIdentifier; assert(`Expected a record`, isStableIdentifier(newIdentifier)); // We generate "transactions" whenever a setter method on the array @@ -436,6 +487,28 @@ export class IdentifierArray { }, }) as IdentifierArray; + if (DEPRECATE_A_USAGE) { + const meta = Ember.meta(this); + meta.hasMixin = (mixin: object) => { + deprecate(`Do not call A() on EmberData RecordArrays`, false, { + id: 'ember-data:no-a-with-array-like', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + }); + // @ts-expect-error ArrayMixin is more than a type + if (mixin === NativeArray || mixin === ArrayMixin) { + return true; + } + return false; + }; + } else if (DEBUG) { + const meta = Ember.meta(this); + meta.hasMixin = (mixin: object) => { + assert(`Do not call A() on EmberData RecordArrays`); + }; + } + createArrayTags(proxy, _SIGNAL); this[NOTIFY] = this[NOTIFY].bind(proxy); @@ -485,7 +558,7 @@ export class IdentifierArray { } /* - Update this Array and return a promise which resolves once the update + Update this RecordArray and return a promise which resolves once the update is finished. */ _update(): Promise> { @@ -515,9 +588,14 @@ export class IdentifierArray { @public @return {Promise} promise */ - save(): Promise { + save(): Promise> { const promise = Promise.all(this.map((record) => this.store.saveRecord(record))).then(() => this); + if (DEPRECATE_PROMISE_PROXIES) { + // @ts-expect-error IdentifierArray is not a MutableArray + return promiseArray>(promise); + } + return promise; } } @@ -530,7 +608,11 @@ const desc = { enumerable: true, configurable: false, get: function () { - return this; + // here to support computed chains + // and {{#each}} + if (DEPRECATE_COMPUTED_CHAINS) { + return this; + } }, }; compat(desc); @@ -538,6 +620,31 @@ Object.defineProperty(IdentifierArray.prototype, '[]', desc); defineSignal(IdentifierArray.prototype, 'isUpdating', false); +export default IdentifierArray; + +if (DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS) { + Object.defineProperty(IdentifierArray.prototype, 'type', { + get() { + deprecate( + `Using RecordArray.type to access the ModelClass for a record is deprecated. Use store.modelFor() instead.`, + false, + { + id: 'ember-data:deprecate-snapshot-model-class-access', + until: '5.0', + for: 'ember-data', + since: { available: '4.5.0', enabled: '4.5.0' }, + } + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!this.modelName) { + return null; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + return this.store.modelFor(this.modelName); + }, + }); +} + export type CollectionCreateOptions = IdentifierArrayCreateOptions & { manager: RecordArrayManager; query: ImmutableRequestInfo | Record | null; @@ -566,6 +673,11 @@ export class Collection extends IdentifierArray { // both being valid options to pass through here. const promise = store.query(this.modelName, query as Record, { _recordArray: this }); + if (DEPRECATE_PROMISE_PROXIES) { + // @ts-expect-error Collection is not a MutableArray + return promiseArray(promise); + } + return promise; } @@ -579,7 +691,415 @@ export class Collection extends IdentifierArray { Collection.prototype.query = null; // Ensure instanceof works correctly -// Object.setPrototypeOf(IdentifierArray.prototype, Array.prototype); +//Object.setPrototypeOf(IdentifierArray.prototype, Array.prototype); + +if (DEPRECATE_ARRAY_LIKE) { + IdentifierArray.prototype.DEPRECATED_CLASS_NAME = 'RecordArray'; + Collection.prototype.DEPRECATED_CLASS_NAME = 'RecordArray'; + const EmberObjectMethods = [ + 'addObserver', + 'cacheFor', + 'decrementProperty', + 'get', + 'getProperties', + 'incrementProperty', + 'notifyPropertyChange', + 'removeObserver', + 'set', + 'setProperties', + 'toggleProperty', + ] as const; + EmberObjectMethods.forEach((method) => { + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype[method] = function delegatedMethod(...args: unknown[]): unknown { + deprecate( + `The EmberObject ${method} method on the class ${this.DEPRECATED_CLASS_NAME} is deprecated. Use dot-notation javascript get/set access instead.`, + false, + { + id: 'ember-data:deprecate-array-like', + until: '5.0', + since: { enabled: '4.7', available: '4.7' }, + for: 'ember-data', + } + ); + // @ts-expect-error ember is missing types for some methods + return (Ember[method] as (...args: unknown[]) => unknown)(this, ...args); + }; + }); + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.addObject = function (obj: OpaqueRecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'addObject', 'push'); + const index = this.indexOf(obj); + if (index === -1) { + this.push(obj); + } + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.addObjects = function (objs: OpaqueRecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'addObjects', 'push'); + objs.forEach((obj: OpaqueRecordInstance) => { + const index = this.indexOf(obj); + if (index === -1) { + this.push(obj); + } + }); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.popObject = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'popObject', 'pop'); + return this.pop() as OpaqueRecordInstance; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.pushObject = function (obj: OpaqueRecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'pushObject', 'push'); + this.push(obj); + return obj; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.pushObjects = function (objs: OpaqueRecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'pushObjects', 'push'); + this.push(...objs); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.shiftObject = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'shiftObject', 'shift'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.shift()!; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.unshiftObject = function (obj: OpaqueRecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'unshiftObject', 'unshift'); + this.unshift(obj); + return obj; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.unshiftObjects = function (objs: OpaqueRecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'unshiftObjects', 'unshift'); + this.unshift(...objs); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.objectAt = function (index: number) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'objectAt', 'at'); + //For negative index values go back from the end of the array + const arrIndex = Math.sign(index) === -1 ? this.length + index : index; + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this[arrIndex]; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.objectsAt = function (indices: number[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'objectsAt', 'at'); + // @ts-expect-error adding MutableArray method + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return indices.map((index) => this.objectAt(index)!); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.removeAt = function (index: number) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'removeAt', 'splice'); + this.splice(index, 1); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.insertAt = function (index: number, obj: OpaqueRecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'insertAt', 'splice'); + this.splice(index, 0, obj); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.removeObject = function (obj: OpaqueRecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'removeObject', 'splice'); + const index = this.indexOf(obj); + if (index !== -1) { + this.splice(index, 1); + } + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.removeObjects = function (objs: OpaqueRecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'removeObjects', 'splice'); + objs.forEach((obj) => { + const index = this.indexOf(obj); + if (index !== -1) { + this.splice(index, 1); + } + }); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.toArray = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'toArray', 'slice'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.slice(); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.replace = function (idx: number, amt: number, objects?: OpaqueRecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'replace', 'splice'); + if (objects) { + this.splice(idx, amt, ...objects); + } else { + this.splice(idx, amt); + } + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.clear = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'clear', 'length = 0'); + this.splice(0, this.length); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.setObjects = function (objects: OpaqueRecordInstance[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'setObjects', '`arr.length = 0; arr.push(objects);`'); + assert( + `${this.DEPRECATED_CLASS_NAME}.setObjects expects to receive an array as its argument`, + Array.isArray(objects) + ); + this.splice(0, this.length); + this.push(...objects); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.reverseObjects = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'reverseObjects', 'reverse'); + this.reverse(); + return this; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.compact = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'compact', 'filter'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.filter((v) => v !== null && v !== undefined); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.any = function (callback, target) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'any', 'some'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return this.some(callback, target); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.isAny = function (prop, value) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'isAny', 'some'); + const hasValue = arguments.length === 2; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return this.some((v) => (hasValue ? v[prop] === value : v[prop] === true)); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.isEvery = function (prop, value) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'isEvery', 'every'); + const hasValue = arguments.length === 2; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return this.every((v) => (hasValue ? v[prop] === value : v[prop] === true)); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.getEach = function (key: string) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'getEach', 'map'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.map((value) => get(value, key)); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.mapBy = function (key: string) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'mapBy', 'map'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.map((value) => get(value, key)); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.findBy = function (key: string, value?: unknown) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'findBy', 'find'); + if (arguments.length === 2) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.find((val) => { + return get(val, key) === value; + }); + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.find((val) => Boolean(get(val, key))); + } + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.filterBy = function (key: string, value?: unknown) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'filterBy', 'filter'); + if (arguments.length === 2) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.filter((record) => { + return get(record, key) === value; + }); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.filter((record) => { + return Boolean(get(record, key)); + }); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.sortBy = function (...sortKeys: string[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'sortBy', '.slice().sort'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.slice().sort((a, b) => { + for (let i = 0; i < sortKeys.length; i++) { + const key = sortKeys[i]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const propA = get(a, key); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const propB = get(b, key); + // return 1 or -1 else continue to the next sortKey + const compareValue = compare(propA, propB); + + if (compareValue) { + return compareValue; + } + } + return 0; + }); + }; + + // @ts-expect-error + IdentifierArray.prototype.invoke = function (key: string, ...args: unknown[]) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'invoke', 'forEach'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return this.map((value) => (value[key] as (...args: unknown[]) => unknown)(...args)); + }; + + // @ts-expect-error + IdentifierArray.prototype.addArrayObserver = function () { + deprecateArrayLike( + this.DEPRECATED_CLASS_NAME, + 'addArrayObserver', + 'derived state or reacting at the change source' + ); + }; + + // @ts-expect-error + IdentifierArray.prototype.removeArrayObserver = function () { + deprecateArrayLike( + this.DEPRECATED_CLASS_NAME, + 'removeArrayObserver', + 'derived state or reacting at the change source' + ); + }; + + // @ts-expect-error + IdentifierArray.prototype.arrayContentWillChange = function () { + deprecateArrayLike( + this.DEPRECATED_CLASS_NAME, + 'arrayContentWillChange', + 'derived state or reacting at the change source' + ); + }; + + // @ts-expect-error + IdentifierArray.prototype.arrayContentDidChange = function () { + deprecateArrayLike( + this.DEPRECATED_CLASS_NAME, + 'arrayContentDidChange', + 'derived state or reacting at the change source.' + ); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.reject = function (callback, target?: unknown) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'reject', 'filter'); + assert('`reject` expects a function as first argument.', typeof callback === 'function'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.filter((...args) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + return !callback.apply(target, args); + }); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.rejectBy = function (key: string, value?: unknown) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'rejectBy', 'filter'); + if (arguments.length === 2) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.filter((record) => { + return get(record, key) !== value; + }); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.filter((record) => { + return !get(record, key); + }); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.setEach = function (key: string, value: unknown) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'setEach', 'forEach'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.forEach((item) => set(item, key, value)); + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.uniq = function () { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'uniq', 'filter'); + // all current managed arrays are already enforced as unique + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return this.slice(); + }; + + // @ts-expect-error + IdentifierArray.prototype.uniqBy = function (key: string) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'uniqBy', 'filter'); + // all current managed arrays are already enforced as unique + const seen = new Set(); + const result: OpaqueRecordInstance[] = []; + this.forEach((item) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const value = get(item, key); + if (seen.has(value)) { + return; + } + seen.add(value); + result.push(item); + }); + return result; + }; + + // @ts-expect-error adding MutableArray method + IdentifierArray.prototype.without = function (value: OpaqueRecordInstance) { + deprecateArrayLike(this.DEPRECATED_CLASS_NAME, 'without', 'slice'); + const newArr = this.slice(); + const index = this.indexOf(value); + if (index !== -1) { + newArr.splice(index, 1); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return newArr; + }; + + // @ts-expect-error + IdentifierArray.prototype.firstObject = null; + // @ts-expect-error + IdentifierArray.prototype.lastObject = null; +} type PromiseProxyRecord = { then(): void; content: OpaqueRecordInstance | null | undefined }; @@ -597,11 +1117,25 @@ function assertRecordPassedToHasMany(record: OpaqueRecordInstance | PromiseProxy ); } -function extractIdentifierFromRecord(record: PromiseProxyRecord | OpaqueRecordInstance | null) { - if (!record) { +function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | OpaqueRecordInstance | null) { + if (!recordOrPromiseRecord) { return null; } - assertRecordPassedToHasMany(record); - return recordIdentifierFor(record); + if (isPromiseRecord(recordOrPromiseRecord)) { + const content = recordOrPromiseRecord.content; + assert( + 'You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo relationship.', + content !== undefined && content !== null + ); + assertRecordPassedToHasMany(content); + return recordIdentifierFor(content); + } + + assertRecordPassedToHasMany(recordOrPromiseRecord); + return recordIdentifierFor(recordOrPromiseRecord); +} + +function isPromiseRecord(record: PromiseProxyRecord | OpaqueRecordInstance): record is PromiseProxyRecord { + return Boolean(typeof record === 'object' && record && 'then' in record); } diff --git a/packages/store/src/-private/store-service.ts b/packages/store/src/-private/store-service.ts index 2405687a75f..00323a16347 100644 --- a/packages/store/src/-private/store-service.ts +++ b/packages/store/src/-private/store-service.ts @@ -10,7 +10,11 @@ import type RequestManager from '@ember-data/request'; import type { Future } from '@ember-data/request'; import { LOG_PAYLOADS, LOG_REQUESTS } from '@warp-drive/build-config/debugging'; import { + DEPRECATE_HAS_RECORD, + DEPRECATE_PROMISE_PROXIES, DEPRECATE_STORE_EXTENDS_EMBER_OBJECT, + DEPRECATE_STORE_FIND, + DISABLE_6X_DEPRECATIONS, ENABLE_LEGACY_SCHEMA_SERVICE, } from '@warp-drive/build-config/deprecations'; import { DEBUG, TESTING } from '@warp-drive/build-config/env'; @@ -57,6 +61,7 @@ import { CacheManager } from './managers/cache-manager'; import NotificationManager from './managers/notification-manager'; import { RecordArrayManager } from './managers/record-array-manager'; import { RequestPromise, RequestStateService } from './network/request-cache'; +import { promiseArray, promiseObject } from './proxies/promise-proxies'; import type { Collection, IdentifierArray } from './record-arrays/identifier-array'; import { coerceId, ensureStringId } from './utils/coerce-id'; import { constructResource } from './utils/construct-resource'; @@ -215,7 +220,7 @@ const app = new EmberApp(defaults, { }); \`\`\` `, - false, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, { id: 'ember-data:deprecate-store-extends-ember-object', until: '6.0', @@ -1021,6 +1026,60 @@ export class Store extends BaseClass { } } + /** + @method find + @param {String} modelName + @param {String|Integer} id + @param {Object} options + @return {Promise} promise + @deprecated + @private + */ + find(modelName: string, id: string | number, options?: FindRecordOptions): Promise { + if (DEBUG) { + assertDestroyingStore(this, 'find'); + } + // The default `model` hook in Route calls `find(modelName, id)`, + // that's why we have to keep this method around even though `findRecord` is + // the public way to get a record by modelName and id. + assert( + `Using store.find(type) has been removed. Use store.findAll(modelName) to retrieve all records for a given type.`, + arguments.length !== 1 + ); + assert( + `Calling store.find(modelName, id, { preload: preload }) is no longer supported. Use store.findRecord(modelName, id, { preload: preload }) instead.`, + !options + ); + assert(`You need to pass the model name and id to the store's find method`, arguments.length === 2); + assert( + `You cannot pass '${id}' as id to the store's find method`, + typeof id === 'string' || typeof id === 'number' + ); + assert( + `Calling store.find() with a query object is no longer supported. Use store.query() instead.`, + typeof id !== 'object' + ); + assert( + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, + typeof modelName === 'string' + ); + + if (DEPRECATE_STORE_FIND) { + deprecate( + `Using store.find is deprecated, use store.findRecord instead. Likely this means you are relying on the implicit store fetching behavior of routes unknowingly.`, + false, + { + id: 'ember-data:deprecate-store-find', + since: { available: '4.5', enabled: '4.5' }, + for: 'ember-data', + until: '5.0', + } + ); + return this.findRecord(modelName, id); + } + assert(`store.find has been removed. Use store.findRecord instead.`); + } + /** This method returns a record for a given identifier or type and id combination. @@ -1427,6 +1486,14 @@ export class Store extends BaseClass { cacheOptions: { [SkipCache]: true }, }); + if (DEPRECATE_PROMISE_PROXIES) { + return promiseObject( + promise.then((document) => { + return document.content; + }) + ); + } + return promise.then((document) => { return document.content; }); @@ -1577,6 +1644,54 @@ export class Store extends BaseClass { return isLoaded ? (this._instanceCache.getRecord(stableIdentifier) as T) : null; } + /** + This method returns true if a record for a given modelName and id is already + loaded in the store. Use this function to know beforehand if a findRecord() + will result in a request or that it will be a cache hit. + + Example + + ```javascript + store.hasRecordForId('post', 1); // false + store.findRecord('post', 1).then(function() { + store.hasRecordForId('post', 1); // true + }); + ``` + + @method hasRecordForId + @deprecated + @public + @param {String} modelName + @param {(String|Integer)} id + @return {Boolean} + */ + hasRecordForId(modelName: string, id: string | number): boolean { + if (DEPRECATE_HAS_RECORD) { + deprecate(`store.hasRecordForId has been deprecated in favor of store.peekRecord`, false, { + id: 'ember-data:deprecate-has-record-for-id', + since: { available: '4.5', enabled: '4.5' }, + until: '5.0', + for: 'ember-data', + }); + if (DEBUG) { + assertDestroyingStore(this, 'hasRecordForId'); + } + assert(`You need to pass a model name to the store's hasRecordForId method`, modelName); + assert( + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, + typeof modelName === 'string' + ); + + const type = normalizeModelName(modelName); + const trueId = ensureStringId(id); + const resource = { type, id: trueId }; + + const identifier = this.identifierCache.peekRecordIdentifier(resource); + return Boolean(identifier && this._instanceCache.recordIsLoaded(identifier)); + } + assert(`store.hasRecordForId has been removed`); + } + /** This method delegates a query to the adapter. This is the one place where adapter-level semantics are exposed to the application. @@ -1651,6 +1766,9 @@ export class Store extends BaseClass { cacheOptions: { [SkipCache]: true }, }); + if (DEPRECATE_PROMISE_PROXIES) { + return promiseArray(promise.then((document) => document.content)) as unknown as Promise; + } return promise.then((document) => document.content); } @@ -1779,6 +1897,10 @@ export class Store extends BaseClass { cacheOptions: { [SkipCache]: true }, }); + if (DEPRECATE_PROMISE_PROXIES) { + return promiseObject(promise.then((document) => document.content)); + } + return promise.then((document) => document.content); } @@ -1981,6 +2103,10 @@ export class Store extends BaseClass { cacheOptions: { [SkipCache]: true }, }); + if (DEPRECATE_PROMISE_PROXIES) { + return promiseArray(promise.then((document) => document.content)) as unknown as Promise>; + } + return promise.then((document) => document.content); } @@ -2392,39 +2518,51 @@ export class Store extends BaseClass { if (ENABLE_LEGACY_SCHEMA_SERVICE) { Store.prototype.getSchemaDefinitionService = function (): SchemaService { assert(`You must registerSchemaDefinitionService with the store to use custom model classes`, this._schema); - deprecate(`Use \`store.schema\` instead of \`store.getSchemaDefinitionService()\``, false, { - id: 'ember-data:schema-service-updates', - until: '6.0', - for: 'ember-data', - since: { - available: '4.13', - enabled: '5.4', - }, - }); + deprecate( + `Use \`store.schema\` instead of \`store.getSchemaDefinitionService()\``, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:schema-service-updates', + until: '6.0', + for: 'ember-data', + since: { + available: '4.13', + enabled: '5.4', + }, + } + ); return this._schema; }; Store.prototype.registerSchemaDefinitionService = function (schema: SchemaService) { - deprecate(`Use \`store.createSchemaService\` instead of \`store.registerSchemaDefinitionService()\``, false, { - id: 'ember-data:schema-service-updates', - until: '6.0', - for: 'ember-data', - since: { - available: '4.13', - enabled: '5.4', - }, - }); + deprecate( + `Use \`store.createSchemaService\` instead of \`store.registerSchemaDefinitionService()\``, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:schema-service-updates', + until: '6.0', + for: 'ember-data', + since: { + available: '4.13', + enabled: '5.4', + }, + } + ); this._schema = schema; }; Store.prototype.registerSchema = function (schema: SchemaService) { - deprecate(`Use \`store.createSchemaService\` instead of \`store.registerSchema()\``, false, { - id: 'ember-data:schema-service-updates', - until: '6.0', - for: 'ember-data', - since: { - available: '4.13', - enabled: '5.4', - }, - }); + deprecate( + `Use \`store.createSchemaService\` instead of \`store.registerSchema()\``, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS, + { + id: 'ember-data:schema-service-updates', + until: '6.0', + for: 'ember-data', + since: { + available: '4.13', + enabled: '5.4', + }, + } + ); this._schema = schema; }; } @@ -2534,5 +2672,33 @@ function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | } const extract = recordIdentifierFor; + if (DEPRECATE_PROMISE_PROXIES) { + if (isPromiseRecord(recordOrPromiseRecord)) { + const content = recordOrPromiseRecord.content; + assert( + 'You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo or hasMany relationship to the get call.', + content !== undefined + ); + deprecate( + `You passed in a PromiseProxy to a Relationship API that now expects a resolved value. await the value before setting it.`, + false, + { + id: 'ember-data:deprecate-promise-proxies', + until: '5.0', + since: { + enabled: '4.7', + available: '4.7', + }, + for: 'ember-data', + } + ); + return content ? extract(content) : null; + } + } + return extract(recordOrPromiseRecord); } + +function isPromiseRecord(record: PromiseProxyRecord | OpaqueRecordInstance): record is PromiseProxyRecord { + return typeof record === 'object' && !!record && 'then' in record && typeof record.then === 'function'; +} diff --git a/packages/store/src/-private/utils/coerce-id.ts b/packages/store/src/-private/utils/coerce-id.ts index ceb1f6f8dda..2be265ef878 100644 --- a/packages/store/src/-private/utils/coerce-id.ts +++ b/packages/store/src/-private/utils/coerce-id.ts @@ -4,7 +4,7 @@ import { deprecate } from '@ember/debug'; -import { DEPRECATE_NON_STRICT_ID } from '@warp-drive/build-config/deprecations'; +import { DEPRECATE_NON_STRICT_ID, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; // Used by the store to normalize IDs entering the store. Despite the fact @@ -28,7 +28,7 @@ export function coerceId(id: unknown): string | null { `The resource id '<${typeof id}> ${String( id )} ' is not normalized. Update your application code to use '${JSON.stringify(normalized)}' instead.`, - normalized === id, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : normalized === id, { id: 'ember-data:deprecate-non-strict-id', until: '6.0', diff --git a/packages/store/src/-private/utils/normalize-model-name.ts b/packages/store/src/-private/utils/normalize-model-name.ts index 91e12a8e829..dba54a8b1c1 100644 --- a/packages/store/src/-private/utils/normalize-model-name.ts +++ b/packages/store/src/-private/utils/normalize-model-name.ts @@ -1,7 +1,7 @@ import { deprecate } from '@ember/debug'; import { dasherize } from '@ember-data/request-utils/string'; -import { DEPRECATE_NON_STRICT_TYPES } from '@warp-drive/build-config/deprecations'; +import { DEPRECATE_NON_STRICT_TYPES, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; export function normalizeModelName(type: string): string { if (DEPRECATE_NON_STRICT_TYPES) { @@ -9,7 +9,7 @@ export function normalizeModelName(type: string): string { deprecate( `The resource type '${type}' is not normalized. Update your application code to use '${result}' instead of '${type}'.`, - result === type, + /* inline-macro-config */ DISABLE_6X_DEPRECATIONS ? true : result === type, { id: 'ember-data:deprecate-non-strict-types', until: '6.0', diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index 6b31bf43285..c30dab7d595 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -186,6 +186,7 @@ export { Store as default, type StoreRequestContext, CacheHandler, + normalizeModelName, type Document, type CachePolicy, type StoreRequestInput, diff --git a/packages/store/vite.config.mjs b/packages/store/vite.config.mjs index ca9aa0e126f..ba7aaf03493 100644 --- a/packages/store/vite.config.mjs +++ b/packages/store/vite.config.mjs @@ -1,6 +1,20 @@ import { createConfig } from '@warp-drive/internal-config/vite/config.js'; -export const externals = ['@ember/runloop', '@ember/object', '@ember/debug']; +export const externals = [ + 'ember', + '@ember/object/computed', + '@ember/object/promise-proxy-mixin', + '@ember/object/proxy', + '@ember/array/proxy', + '@ember/application', + '@ember/debug', + '@ember/owner', + '@ember/utils', + '@ember/runloop', + '@ember/object', + '@ember/debug', +]; + export const entryPoints = ['./src/index.ts', './src/types.ts', './src/-private.ts']; export default createConfig( diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 130568171cb..18c402d8342 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -1,7 +1,7 @@ { "name": "@ember-data/tracking", "description": "Tracking Primitives for controlling change notification of Tracked properties when working with EmberData", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": false, "license": "MIT", "author": "Chris Thoburn ", diff --git a/packages/unpublished-eslint-rules/package.json b/packages/unpublished-eslint-rules/package.json index 300ea311226..087147bc3e1 100644 --- a/packages/unpublished-eslint-rules/package.json +++ b/packages/unpublished-eslint-rules/package.json @@ -1,7 +1,7 @@ { "name": "eslint-plugin-ember-data-internal", "main": "./src/index.js", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "repository": { "type": "git", diff --git a/packages/unpublished-test-infra/package.json b/packages/unpublished-test-infra/package.json index 76886dea1b6..d78bd734555 100644 --- a/packages/unpublished-test-infra/package.json +++ b/packages/unpublished-test-infra/package.json @@ -1,6 +1,6 @@ { "name": "@ember-data/unpublished-test-infra", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "description": "The default blueprint for ember-data private packages.", "keywords": [ diff --git a/release/strategy.json b/release/strategy.json index 3fa6ae166fd..2146afbc7d2 100644 --- a/release/strategy.json +++ b/release/strategy.json @@ -30,74 +30,20 @@ "typesPublish": true }, "rules": { - "@ember-data/codemods": { - "stage": "stable", - "types": "private", - "typesPublish": false, - "mirrorPublish": false - }, "@ember-data/debug": { "stage": "stable", "types": "private", "typesPublish": false }, - "warp-drive": { - "stage": "alpha", - "types": "private", - "typesPublish": false, - "mirrorPublish": false - }, "@warp-drive/build-config": { "stage": "alpha", "types": "alpha", "typesPublish": false, "mirrorPublish": true }, - "@warp-drive/holodeck": { - "stage": "alpha", - "types": "private", - "typesPublish": false, - "mirrorPublish": false - }, - "@warp-drive/diagnostic": { - "stage": "alpha", - "types": "private", - "typesPublish": false, - "mirrorPublish": false - }, - "eslint-plugin-ember-data": { - "stage": "alpha", - "types": "private", - "typesPublish": false, - "mirrorPublish": false - }, - "eslint-plugin-warp-drive": { - "stage": "alpha", - "types": "private", - "typesPublish": false, - "mirrorPublish": false - }, "@warp-drive/core-types": { "stage": "alpha", "types": "alpha" - }, - "@warp-drive/ember": { - "stage": "alpha", - "types": "alpha", - "typesPublish": false, - "mirrorPublish": false - }, - "@warp-drive/schema": { - "stage": "alpha", - "types": "private", - "typesPublish": false, - "mirrorPublish": false - }, - "@warp-drive/schema-record": { - "stage": "alpha", - "types": "alpha", - "typesPublish": false, - "mirrorPublish": true } } } diff --git a/tests/blueprints/package.json b/tests/blueprints/package.json index 3ed8e3625af..5e1718cde57 100644 --- a/tests/blueprints/package.json +++ b/tests/blueprints/package.json @@ -1,6 +1,6 @@ { "name": "blueprint-tests", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "description": "Provides tests for blueprints", "repository": { diff --git a/tests/builders/package.json b/tests/builders/package.json index d2fd7a8cbf9..d6373c3874b 100644 --- a/tests/builders/package.json +++ b/tests/builders/package.json @@ -1,6 +1,6 @@ { "name": "builders-test-app", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "description": "Provides tests for URL and Request Building Capabilities", "keywords": [], diff --git a/tests/docs/fixtures/expected.js b/tests/docs/fixtures/expected.js index 9ce30a7bc97..26d281b8e3f 100644 --- a/tests/docs/fixtures/expected.js +++ b/tests/docs/fixtures/expected.js @@ -30,27 +30,6 @@ module.exports = { 'ember-data-overview', ], classitems: [ - '(public) @ember-data/store Document#data', - '(public) @ember-data/store Document#errors', - '(public) @ember-data/store Document#fetch', - '(public) @ember-data/store Document#first', - '(public) @ember-data/store Document#identifier', - '(public) @ember-data/store Document#last', - '(public) @ember-data/store Document#links', - '(public) @ember-data/store Document#meta', - '(public) @ember-data/store Document#next', - '(public) @ember-data/store Document#prev', - '(public) @ember-data/store Document#toJSON', - '(public) @ember-data/store RequestStateService#getLastRequestForRecord', - '(public) @ember-data/store RequestStateService#getPendingRequestsForRecord', - '(public) @ember-data/store RequestStateService#subscribeForRecord', - '(public) @ember-data/store IdentifierCache#getOrCreateDocumentIdentifier', - '(public) @ember-data/store Store#registerSchema', - '(public) @ember-data/store Store#schema', - '(public) @ember-data/store CachePolicy#didRequest [Optional]', - '(public) @ember-data/store CachePolicy#isHardExpired', - '(public) @ember-data/store CachePolicy#isSoftExpired', - '(public) @ember-data/store CachePolicy#willRequest [Optional]', '(private) @ember-data/adapter BuildURLMixin#_buildURL', '(private) @ember-data/adapter BuildURLMixin#urlPrefix', '(private) @ember-data/adapter/json-api JSONAPIAdapter#ajaxOptions', @@ -72,28 +51,6 @@ module.exports = { '(private) @ember-data/debug InspectorDataAdapter#observeRecord', '(private) @ember-data/debug InspectorDataAdapter#watchModelTypes', '(private) @ember-data/debug InspectorDataAdapter#watchTypeIfUnseen', - '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_COMPUTED_CHAINS', - '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_LEGACY_IMPORTS', - '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_MANY_ARRAY_DUPLICATES', - '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_NON_STRICT_ID', - '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_NON_STRICT_TYPES', - '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_NON_UNIQUE_PAYLOADS', - '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE', - '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_STORE_EXTENDS_EMBER_OBJECT', - '(public) @warp-drive/build-config/deprecations CurrentDeprecations#ENABLE_LEGACY_SCHEMA_SERVICE', - '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_EMBER_INFLECTOR', - '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#findAll', - '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#findRecord', - '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#query', - '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#queryRecord', - '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#saveRecord', - '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#configureAssertFn', - '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#configureMismatchReporter', - '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#configureTypeNormalization', - '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#formattedId', - '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#formattedType', - '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#isEquivId', - '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#isEquivType', '(private) @ember-data/legacy-compat SnapshotRecordArray#_recordArray', '(private) @ember-data/legacy-compat SnapshotRecordArray#_snapshots', '(private) @ember-data/legacy-compat SnapshotRecordArray#constructor', @@ -134,11 +91,12 @@ module.exports = { '(private) @ember-data/store RecordArray#store', '(private) @ember-data/store Snapshot#constructor', '(private) @ember-data/store Store#_push', + '(private) @ember-data/store Store#find', '(private) @ember-data/store Store#init', - '(public) @ember-data/active-record/request @ember-data/active-record/request#query', - '(public) @ember-data/active-record/request @ember-data/active-record/request#findRecord', '(public) @ember-data/active-record/request @ember-data/active-record/request#createRecord', '(public) @ember-data/active-record/request @ember-data/active-record/request#deleteRecord', + '(public) @ember-data/active-record/request @ember-data/active-record/request#findRecord', + '(public) @ember-data/active-record/request @ember-data/active-record/request#query', '(public) @ember-data/active-record/request @ember-data/active-record/request#updateRecord', '(public) @ember-data/adapter Adapter#coalesceFindRequests', '(public) @ember-data/adapter Adapter#createRecord', @@ -168,8 +126,10 @@ module.exports = { '(public) @ember-data/adapter BuildURLMixin#urlForQuery', '(public) @ember-data/adapter BuildURLMixin#urlForQueryRecord', '(public) @ember-data/adapter BuildURLMixin#urlForUpdateRecord', - '(public) @ember-data/adapter/json-api JSONAPIAdapter#coalesceFindRequests', + '(public) @ember-data/adapter/error @ember-data/adapter/error#errorsArrayToHash', + '(public) @ember-data/adapter/error @ember-data/adapter/error#errorsHashToArray', '(public) @ember-data/adapter/json-api JSONAPIAdapter#buildQuery', + '(public) @ember-data/adapter/json-api JSONAPIAdapter#coalesceFindRequests', '(public) @ember-data/adapter/rest RESTAdapter#buildQuery', '(public) @ember-data/adapter/rest RESTAdapter#coalesceFindRequests', '(public) @ember-data/adapter/rest RESTAdapter#createRecord', @@ -191,24 +151,15 @@ module.exports = { '(public) @ember-data/adapter/rest RESTAdapter#sortQueryParams', '(public) @ember-data/adapter/rest RESTAdapter#updateRecord', '(public) @ember-data/adapter/rest RESTAdapter#useFetch', - '(public) @warp-drive/build-config/debugging DebugLogging#LOG_GRAPH', - '(public) @warp-drive/build-config/debugging DebugLogging#LOG_IDENTIFIERS', - '(public) @warp-drive/build-config/debugging DebugLogging#LOG_INSTANCE_CACHE', - '(public) @warp-drive/build-config/debugging DebugLogging#LOG_MUTATIONS', - '(public) @warp-drive/build-config/debugging DebugLogging#LOG_NOTIFICATIONS', - '(public) @warp-drive/build-config/debugging DebugLogging#LOG_OPERATIONS', - '(public) @warp-drive/build-config/debugging DebugLogging#LOG_PAYLOADS', - '(public) @warp-drive/build-config/debugging DebugLogging#LOG_REQUESTS', - '(public) @warp-drive/build-config/debugging DebugLogging#LOG_REQUEST_STATUS', '(public) @ember-data/experimental-preview-types Adapter#coalesceFindRequests [OPTIONAL]', '(public) @ember-data/experimental-preview-types Adapter#createRecord', '(public) @ember-data/experimental-preview-types Adapter#deleteRecord', '(public) @ember-data/experimental-preview-types Adapter#destroy [OPTIONAL]', '(public) @ember-data/experimental-preview-types Adapter#findAll', '(public) @ember-data/experimental-preview-types Adapter#findBelongsTo [OPTIONAL]', + '(public) @ember-data/experimental-preview-types Adapter#findhasMany [OPTIONAL]', '(public) @ember-data/experimental-preview-types Adapter#findMany [OPTIONAL]', '(public) @ember-data/experimental-preview-types Adapter#findRecord', - '(public) @ember-data/experimental-preview-types Adapter#findhasMany [OPTIONAL]', '(public) @ember-data/experimental-preview-types Adapter#generateIdForRecord [OPTIONAL]', '(public) @ember-data/experimental-preview-types Adapter#groupRecordsForFindMany [OPTIONAL]', '(public) @ember-data/experimental-preview-types Adapter#query', @@ -219,6 +170,7 @@ module.exports = { '(public) @ember-data/experimental-preview-types Adapter#shouldReloadRecord [OPTIONAL]', '(public) @ember-data/experimental-preview-types Adapter#updateRecord', '(public) @ember-data/experimental-preview-types Cache#changedAttrs', + '(public) @ember-data/experimental-preview-types Cache#changedRelationships', '(public) @ember-data/experimental-preview-types Cache#clientDidCreate', '(public) @ember-data/experimental-preview-types Cache#commitWasRejected', '(public) @ember-data/experimental-preview-types Cache#didCommit', @@ -229,6 +181,7 @@ module.exports = { '(public) @ember-data/experimental-preview-types Cache#getErrors', '(public) @ember-data/experimental-preview-types Cache#getRelationship', '(public) @ember-data/experimental-preview-types Cache#hasChangedAttrs', + '(public) @ember-data/experimental-preview-types Cache#hasChangedRelationships', '(public) @ember-data/experimental-preview-types Cache#hydrate', '(public) @ember-data/experimental-preview-types Cache#isDeleted', '(public) @ember-data/experimental-preview-types Cache#isDeletionCommitted', @@ -241,15 +194,13 @@ module.exports = { '(public) @ember-data/experimental-preview-types Cache#peekRequest', '(public) @ember-data/experimental-preview-types Cache#put', '(public) @ember-data/experimental-preview-types Cache#rollbackAttrs', + '(public) @ember-data/experimental-preview-types Cache#rollbackRelationships', '(public) @ember-data/experimental-preview-types Cache#setAttr', '(public) @ember-data/experimental-preview-types Cache#setIsDeleted', '(public) @ember-data/experimental-preview-types Cache#unloadRecord', '(public) @ember-data/experimental-preview-types Cache#upsert', '(public) @ember-data/experimental-preview-types Cache#version', '(public) @ember-data/experimental-preview-types Cache#willCommit', - '(public) @ember-data/experimental-preview-types Cache#changedRelationships', - '(public) @ember-data/experimental-preview-types Cache#hasChangedRelationships', - '(public) @ember-data/experimental-preview-types Cache#rollbackRelationships', '(public) @ember-data/experimental-preview-types Serializer#destroy [OPTIONAL]', '(public) @ember-data/experimental-preview-types Serializer#normalize [OPTIONAL]', '(public) @ember-data/experimental-preview-types Serializer#normalizeResponse', @@ -257,6 +208,7 @@ module.exports = { '(public) @ember-data/experimental-preview-types Serializer#serialize', '(public) @ember-data/experimental-preview-types Serializer#serializeIntoHash [OPTIONAL]', '(public) @ember-data/json-api Cache#changedAttrs', + '(public) @ember-data/json-api Cache#changedRelationships', '(public) @ember-data/json-api Cache#clientDidCreate', '(public) @ember-data/json-api Cache#commitWasRejected', '(public) @ember-data/json-api Cache#didCommit', @@ -266,6 +218,7 @@ module.exports = { '(public) @ember-data/json-api Cache#getErrors', '(public) @ember-data/json-api Cache#getRelationship', '(public) @ember-data/json-api Cache#hasChangedAttrs', + '(public) @ember-data/json-api Cache#hasChangedRelationships', '(public) @ember-data/json-api Cache#hydrate', '(public) @ember-data/json-api Cache#isDeleted', '(public) @ember-data/json-api Cache#isDeletionCommitted', @@ -278,37 +231,47 @@ module.exports = { '(public) @ember-data/json-api Cache#peekRequest', '(public) @ember-data/json-api Cache#put', '(public) @ember-data/json-api Cache#rollbackAttrs', + '(public) @ember-data/json-api Cache#rollbackRelationships', '(public) @ember-data/json-api Cache#setAttr', '(public) @ember-data/json-api Cache#setIsDeleted', '(public) @ember-data/json-api Cache#unloadRecord', '(public) @ember-data/json-api Cache#upsert', '(public) @ember-data/json-api Cache#version', '(public) @ember-data/json-api Cache#willCommit', - '(public) @ember-data/json-api Cache#changedRelationships', - '(public) @ember-data/json-api Cache#hasChangedRelationships', - '(public) @ember-data/json-api Cache#rollbackRelationships', '(public) @ember-data/json-api/request @ember-data/json-api/request#buildQueryParams', - '(public) @ember-data/json-api/request @ember-data/json-api/request#findRecord', - '(public) @ember-data/json-api/request @ember-data/json-api/request#query', - '(public) @ember-data/json-api/request @ember-data/json-api/request#postQuery', '(public) @ember-data/json-api/request @ember-data/json-api/request#createRecord', '(public) @ember-data/json-api/request @ember-data/json-api/request#deleteRecord', - '(public) @ember-data/json-api/request @ember-data/json-api/request#updateRecord', + '(public) @ember-data/json-api/request @ember-data/json-api/request#findRecord', + '(public) @ember-data/json-api/request @ember-data/json-api/request#postQuery', + '(public) @ember-data/json-api/request @ember-data/json-api/request#query', '(public) @ember-data/json-api/request @ember-data/json-api/request#serializePatch', '(public) @ember-data/json-api/request @ember-data/json-api/request#serializeResources', '(public) @ember-data/json-api/request @ember-data/json-api/request#setBuildURLConfig', + '(public) @ember-data/json-api/request @ember-data/json-api/request#updateRecord', '(public) @ember-data/legacy-compat SnapshotRecordArray#adapterOptions', '(public) @ember-data/legacy-compat SnapshotRecordArray#include', '(public) @ember-data/legacy-compat SnapshotRecordArray#length', '(public) @ember-data/legacy-compat SnapshotRecordArray#modelName', '(public) @ember-data/legacy-compat SnapshotRecordArray#snapshots', + '(public) @ember-data/legacy-compat SnapshotRecordArray#type', + '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#findAll', + '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#findRecord', + '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#query', + '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#queryRecord', + '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#saveRecord', + '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#configureAssertFn', + '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#configureMismatchReporter', + '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#configureTypeNormalization', + '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#formattedId', + '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#formattedType', + '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#isEquivId', + '(public) @ember-data/legacy-compat/utils @ember-data/legacy-compat/utils#isEquivType', '(public) @ember-data/model @ember-data/model#attr', '(public) @ember-data/model @ember-data/model#belongsTo', '(public) @ember-data/model @ember-data/model#hasMany', '(public) @ember-data/model BelongsToReference#id', '(public) @ember-data/model BelongsToReference#identifier', '(public) @ember-data/model BelongsToReference#key', - '(public) @ember-data/model BelongsToReference#type', '(public) @ember-data/model BelongsToReference#link', '(public) @ember-data/model BelongsToReference#links', '(public) @ember-data/model BelongsToReference#load', @@ -316,6 +279,7 @@ module.exports = { '(public) @ember-data/model BelongsToReference#push', '(public) @ember-data/model BelongsToReference#reload', '(public) @ember-data/model BelongsToReference#remoteType', + '(public) @ember-data/model BelongsToReference#type', '(public) @ember-data/model BelongsToReference#value', '(public) @ember-data/model BelongsToReference#value', '(public) @ember-data/model Errors#add', @@ -329,7 +293,6 @@ module.exports = { '(public) @ember-data/model HasManyReference#identifiers', '(public) @ember-data/model HasManyReference#ids', '(public) @ember-data/model HasManyReference#key', - '(public) @ember-data/model HasManyReference#type', '(public) @ember-data/model HasManyReference#link', '(public) @ember-data/model HasManyReference#links', '(public) @ember-data/model HasManyReference#load', @@ -337,6 +300,7 @@ module.exports = { '(public) @ember-data/model HasManyReference#push', '(public) @ember-data/model HasManyReference#reload', '(public) @ember-data/model HasManyReference#remoteType', + '(public) @ember-data/model HasManyReference#type', '(public) @ember-data/model HasManyReference#value', '(public) @ember-data/model Model#adapterError', '(public) @ember-data/model Model#attributes', @@ -389,16 +353,16 @@ module.exports = { '(public) @ember-data/model PromiseManyArray#meta', '(public) @ember-data/model PromiseManyArray#reload', '(public) @ember-data/model PromiseManyArray#then', - '(public) @ember-data/request RequestManager#request', - '(public) @ember-data/request RequestManager#use', - '(public) @ember-data/request RequestManager#useCache', '(public) @ember-data/request CacheHandler#request', '(public) @ember-data/request Handler#request', '(public) @ember-data/request Future#abort', '(public) @ember-data/request Future#getStream', - '(public) @ember-data/request Future#onFinalize', '(public) @ember-data/request Future#id', '(public) @ember-data/request Future#lid', + '(public) @ember-data/request Future#onFinalize', + '(public) @ember-data/request RequestManager#request', + '(public) @ember-data/request RequestManager#use', + '(public) @ember-data/request RequestManager#useCache', '(public) @ember-data/request-utils @ember-data/request-utils#buildBaseURL', '(public) @ember-data/request-utils @ember-data/request-utils#buildQueryParams', '(public) @ember-data/request-utils @ember-data/request-utils#filterEmpty', @@ -410,10 +374,10 @@ module.exports = { '(public) @ember-data/request-utils CachePolicy#invalidateRequestsForType', '(public) @ember-data/request-utils CachePolicy#isHardExpired', '(public) @ember-data/request-utils CachePolicy#isSoftExpired', - '(public) @ember-data/rest/request @ember-data/rest/request#findRecord', - '(public) @ember-data/rest/request @ember-data/rest/request#query', '(public) @ember-data/rest/request @ember-data/rest/request#createRecord', '(public) @ember-data/rest/request @ember-data/rest/request#deleteRecord', + '(public) @ember-data/rest/request @ember-data/rest/request#findRecord', + '(public) @ember-data/rest/request @ember-data/rest/request#query', '(public) @ember-data/rest/request @ember-data/rest/request#updateRecord', '(public) @ember-data/serializer Serializer#normalize', '(public) @ember-data/serializer Serializer#normalizeResponse', @@ -477,13 +441,42 @@ module.exports = { '(public) @ember-data/serializer/rest RESTSerializer#serialize', '(public) @ember-data/serializer/rest RESTSerializer#serializeIntoHash', '(public) @ember-data/serializer/rest RESTSerializer#serializePolymorphicType', + '(public) @ember-data/store @ember-data/store#normalizeModelName', '(public) @ember-data/store @ember-data/store#recordIdentifierFor', '(public) @ember-data/store @ember-data/store#setIdentifierForgetMethod', '(public) @ember-data/store @ember-data/store#setIdentifierGenerationMethod', '(public) @ember-data/store @ember-data/store#setIdentifierResetMethod', '(public) @ember-data/store @ember-data/store#setIdentifierUpdateMethod', '(public) @ember-data/store @ember-data/store#setKeyInfoForResource', + '(public) @ember-data/store CachePolicy#didRequest [Optional]', + '(public) @ember-data/store CachePolicy#isHardExpired', + '(public) @ember-data/store CachePolicy#isSoftExpired', + '(public) @ember-data/store CachePolicy#willRequest [Optional]', + '(public) @ember-data/store SchemaService#attributesDefinitionFor', + '(public) @ember-data/store SchemaService#derivation', + '(public) @ember-data/store SchemaService#doesTypeExist', + '(public) @ember-data/store SchemaService#fields', + '(public) @ember-data/store SchemaService#hashFn', + '(public) @ember-data/store SchemaService#hasResource', + '(public) @ember-data/store SchemaService#hasTrait', + '(public) @ember-data/store SchemaService#registerDerivations', + '(public) @ember-data/store SchemaService#registerHashFn', + '(public) @ember-data/store SchemaService#registerResource', + '(public) @ember-data/store SchemaService#registerResources', + '(public) @ember-data/store SchemaService#registerTransformations', + '(public) @ember-data/store SchemaService#relationshipsDefinitionFor', + '(public) @ember-data/store SchemaService#resource', + '(public) @ember-data/store SchemaService#resourceHasTrait', + '(public) @ember-data/store SchemaService#transformation', + '(public) @ember-data/store CacheCapabilitiesManager#disconnectRecord', + '(public) @ember-data/store CacheCapabilitiesManager#getSchemaDefinitionService', + '(public) @ember-data/store CacheCapabilitiesManager#hasRecord', + '(public) @ember-data/store CacheCapabilitiesManager#identifierCache', + '(public) @ember-data/store CacheCapabilitiesManager#notifyChange', + '(public) @ember-data/store CacheCapabilitiesManager#schema', + '(public) @ember-data/store CacheCapabilitiesManager#setRecordId', '(public) @ember-data/store CacheManager#changedAttrs', + '(public) @ember-data/store CacheManager#changedRelationships', '(public) @ember-data/store CacheManager#clientDidCreate', '(public) @ember-data/store CacheManager#commitWasRejected', '(public) @ember-data/store CacheManager#didCommit', @@ -494,6 +487,7 @@ module.exports = { '(public) @ember-data/store CacheManager#getErrors', '(public) @ember-data/store CacheManager#getRelationship', '(public) @ember-data/store CacheManager#hasChangedAttrs', + '(public) @ember-data/store CacheManager#hasChangedRelationships', '(public) @ember-data/store CacheManager#hydrate', '(public) @ember-data/store CacheManager#isDeleted', '(public) @ember-data/store CacheManager#isDeletionCommitted', @@ -506,23 +500,26 @@ module.exports = { '(public) @ember-data/store CacheManager#peekRequest', '(public) @ember-data/store CacheManager#put', '(public) @ember-data/store CacheManager#rollbackAttrs', + '(public) @ember-data/store CacheManager#rollbackRelationships', '(public) @ember-data/store CacheManager#setAttr', '(public) @ember-data/store CacheManager#setIsDeleted', '(public) @ember-data/store CacheManager#unloadRecord', '(public) @ember-data/store CacheManager#upsert', '(public) @ember-data/store CacheManager#willCommit', - '(public) @ember-data/store CacheManager#changedRelationships', - '(public) @ember-data/store CacheManager#hasChangedRelationships', - '(public) @ember-data/store CacheManager#rollbackRelationships', - '(public) @ember-data/store CacheCapabilitiesManager#disconnectRecord', - '(public) @ember-data/store CacheCapabilitiesManager#getSchemaDefinitionService', - '(public) @ember-data/store CacheCapabilitiesManager#schema', - '(public) @ember-data/store CacheCapabilitiesManager#hasRecord', - '(public) @ember-data/store CacheCapabilitiesManager#identifierCache', - '(public) @ember-data/store CacheCapabilitiesManager#notifyChange', - '(public) @ember-data/store CacheCapabilitiesManager#setRecordId', + '(public) @ember-data/store Document#data', + '(public) @ember-data/store Document#errors', + '(public) @ember-data/store Document#fetch', + '(public) @ember-data/store Document#first', + '(public) @ember-data/store Document#identifier', + '(public) @ember-data/store Document#last', + '(public) @ember-data/store Document#links', + '(public) @ember-data/store Document#meta', + '(public) @ember-data/store Document#next', + '(public) @ember-data/store Document#prev', + '(public) @ember-data/store Document#toJSON', '(public) @ember-data/store IdentifierCache#createIdentifierForNewRecord', '(public) @ember-data/store IdentifierCache#forgetRecordIdentifier', + '(public) @ember-data/store IdentifierCache#getOrCreateDocumentIdentifier', '(public) @ember-data/store IdentifierCache#getOrCreateRecordIdentifier', '(public) @ember-data/store IdentifierCache#updateRecordIdentifier', '(public) @ember-data/store ManyArray#createRecord', @@ -536,6 +533,7 @@ module.exports = { '(public) @ember-data/store NotificationManager#unsubscribe', '(public) @ember-data/store RecordArray#isUpdating', '(public) @ember-data/store RecordArray#save', + '(public) @ember-data/store RecordArray#type', '(public) @ember-data/store RecordArray#update', '(public) @ember-data/store RecordReference#id', '(public) @ember-data/store RecordReference#identifier', @@ -544,22 +542,9 @@ module.exports = { '(public) @ember-data/store RecordReference#reload', '(public) @ember-data/store RecordReference#remoteType', '(public) @ember-data/store RecordReference#value', - '(public) @ember-data/store SchemaService#attributesDefinitionFor', - '(public) @ember-data/store SchemaService#doesTypeExist', - '(public) @ember-data/store SchemaService#relationshipsDefinitionFor', - '(public) @ember-data/store SchemaService#derivation', - '(public) @ember-data/store SchemaService#fields', - '(public) @ember-data/store SchemaService#hasResource', - '(public) @ember-data/store SchemaService#hasTrait', - '(public) @ember-data/store SchemaService#hashFn', - '(public) @ember-data/store SchemaService#registerDerivations', - '(public) @ember-data/store SchemaService#registerHashFn', - '(public) @ember-data/store SchemaService#registerResource', - '(public) @ember-data/store SchemaService#registerResources', - '(public) @ember-data/store SchemaService#registerTransformations', - '(public) @ember-data/store SchemaService#resource', - '(public) @ember-data/store SchemaService#resourceHasTrait', - '(public) @ember-data/store SchemaService#transformation', + '(public) @ember-data/store RequestStateService#getLastRequestForRecord', + '(public) @ember-data/store RequestStateService#getPendingRequestsForRecord', + '(public) @ember-data/store RequestStateService#subscribeForRecord', '(public) @ember-data/store Snapshot#adapterOptions', '(public) @ember-data/store Snapshot#attr', '(public) @ember-data/store Snapshot#attributes', @@ -574,6 +559,7 @@ module.exports = { '(public) @ember-data/store Snapshot#modelName', '(public) @ember-data/store Snapshot#record', '(public) @ember-data/store Snapshot#serialize', + '(public) @ember-data/store Snapshot#type', '(public) @ember-data/store StableRecordIdentifier#id', '(public) @ember-data/store StableRecordIdentifier#lid', '(public) @ember-data/store StableRecordIdentifier#type', @@ -587,6 +573,7 @@ module.exports = { '(public) @ember-data/store Store#getReference', '(public) @ember-data/store Store#getRequestStateService', '(public) @ember-data/store Store#getSchemaDefinitionService', + '(public) @ember-data/store Store#hasRecordForId', '(public) @ember-data/store Store#identifierCache', '(public) @ember-data/store Store#identifierCache', '(public) @ember-data/store Store#instantiateRecord (hook)', @@ -600,10 +587,12 @@ module.exports = { '(public) @ember-data/store Store#pushPayload', '(public) @ember-data/store Store#query', '(public) @ember-data/store Store#queryRecord', + '(public) @ember-data/store Store#registerSchema', '(public) @ember-data/store Store#registerSchemaDefinitionService', '(public) @ember-data/store Store#request', '(public) @ember-data/store Store#requestManager', '(public) @ember-data/store Store#saveRecord', + '(public) @ember-data/store Store#schema', '(public) @ember-data/store Store#serializerFor', '(public) @ember-data/store Store#teardownRecord (hook)', '(public) @ember-data/store Store#unloadAll', @@ -611,5 +600,42 @@ module.exports = { '(public) @ember-data/tracking @ember-data/tracking#memoTransact', '(public) @ember-data/tracking @ember-data/tracking#transact', '(public) @ember-data/tracking @ember-data/tracking#untracked', + '(public) @warp-drive/build-config/debugging DebugLogging#LOG_GRAPH', + '(public) @warp-drive/build-config/debugging DebugLogging#LOG_IDENTIFIERS', + '(public) @warp-drive/build-config/debugging DebugLogging#LOG_INSTANCE_CACHE', + '(public) @warp-drive/build-config/debugging DebugLogging#LOG_MUTATIONS', + '(public) @warp-drive/build-config/debugging DebugLogging#LOG_NOTIFICATIONS', + '(public) @warp-drive/build-config/debugging DebugLogging#LOG_OPERATIONS', + '(public) @warp-drive/build-config/debugging DebugLogging#LOG_PAYLOADS', + '(public) @warp-drive/build-config/debugging DebugLogging#LOG_REQUEST_STATUS', + '(public) @warp-drive/build-config/debugging DebugLogging#LOG_REQUESTS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_A_USAGE', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_ARRAY_LIKE', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_COMPUTED_CHAINS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_EARLY_STATIC', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_EMBER_INFLECTOR', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_HAS_RECORD', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_HELPERS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_JSON_API_FALLBACK', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_MANY_ARRAY_DUPLICATES', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_MODEL_REOPEN', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_NON_EXPLICIT_POLYMORPHISM', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_NON_UNIQUE_PAYLOADS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_PROMISE_PROXIES', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_RELATIONSHIPS_WITHOUT_ASYNC', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_RELATIONSHIPS_WITHOUT_TYPE', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_NON_STRICT_ID', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_NON_STRICT_TYPES', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_RSVP_PROMISE', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_SAVE_PROMISE_ACCESS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_SNAPSHOT_MODEL_CLASS_ACCESS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_STORE_EXTENDS_EMBER_OBJECT', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_STORE_FIND', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_STRING_ARG_SCHEMAS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DISABLE_6X_DEPRECATIONS', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#ENABLE_LEGACY_SCHEMA_SERVICE', ], }; diff --git a/tests/docs/package.json b/tests/docs/package.json index 387a2a5369c..1566bdc5dbf 100644 --- a/tests/docs/package.json +++ b/tests/docs/package.json @@ -1,6 +1,6 @@ { "name": "docs-tests", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "description": "Provides tests for blueprints", "repository": { diff --git a/tests/ember-data__adapter/package.json b/tests/ember-data__adapter/package.json index 92853aba2cd..ba4e36d4ca5 100644 --- a/tests/ember-data__adapter/package.json +++ b/tests/ember-data__adapter/package.json @@ -1,6 +1,6 @@ { "name": "ember-data__adapter", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "description": "Tests for @ember-data/adapter", "repository": { diff --git a/tests/ember-data__graph/ember-cli-build.js b/tests/ember-data__graph/ember-cli-build.js index 436ede741f6..837d9d3e6d3 100644 --- a/tests/ember-data__graph/ember-cli-build.js +++ b/tests/ember-data__graph/ember-cli-build.js @@ -20,6 +20,9 @@ module.exports = async function (defaults) { setConfig(app, __dirname, { compatWith: process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null, + deprecations: { + DISABLE_6X_DEPRECATIONS: false, + }, }); app.import('node_modules/@warp-drive/diagnostic/dist/styles/dom-reporter.css'); diff --git a/tests/ember-data__graph/package.json b/tests/ember-data__graph/package.json index 477e6496a1d..cd90cd2169a 100644 --- a/tests/ember-data__graph/package.json +++ b/tests/ember-data__graph/package.json @@ -1,6 +1,6 @@ { "name": "ember-data__graph", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "description": "Provides tests for @ember-data/graph", "keywords": [], diff --git a/tests/ember-data__json-api/package.json b/tests/ember-data__json-api/package.json index 29e3fa67332..b601ec015a9 100644 --- a/tests/ember-data__json-api/package.json +++ b/tests/ember-data__json-api/package.json @@ -1,6 +1,6 @@ { "name": "ember-data__json-api", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "description": "Provides tests for @ember-data/json-api", "keywords": [], diff --git a/tests/ember-data__model/package.json b/tests/ember-data__model/package.json index e68ebd9e794..5f920110d7a 100644 --- a/tests/ember-data__model/package.json +++ b/tests/ember-data__model/package.json @@ -1,6 +1,6 @@ { "name": "ember-data__model", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "description": "Tests for @ember-data/model", "repository": { diff --git a/tests/ember-data__request/package.json b/tests/ember-data__request/package.json index 4052d0392b6..e370f306d17 100644 --- a/tests/ember-data__request/package.json +++ b/tests/ember-data__request/package.json @@ -1,6 +1,6 @@ { "name": "ember-data__request", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "description": "Provides tests for @ember-data/request", "keywords": [], diff --git a/tests/ember-data__serializer/package.json b/tests/ember-data__serializer/package.json index 513f106b91e..a15b2ee512c 100644 --- a/tests/ember-data__serializer/package.json +++ b/tests/ember-data__serializer/package.json @@ -1,6 +1,6 @@ { "name": "ember-data__serializer", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "description": "Tests for the @ember-data/serializer package", "repository": { diff --git a/tests/embroider-basic-compat/package.json b/tests/embroider-basic-compat/package.json index a8feea43f4a..0321496dc43 100644 --- a/tests/embroider-basic-compat/package.json +++ b/tests/embroider-basic-compat/package.json @@ -1,6 +1,6 @@ { "name": "embroider-basic-compat", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "description": "Small description for embroider-basic-compat goes here", "repository": { diff --git a/tests/fastboot/package.json b/tests/fastboot/package.json index 24e972a69c0..f5222e0a193 100644 --- a/tests/fastboot/package.json +++ b/tests/fastboot/package.json @@ -1,6 +1,6 @@ { "name": "fastboot-test-app", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "description": "Small description for fastboot-test-app goes here", "repository": { diff --git a/tests/full-data-asset-size-app/package.json b/tests/full-data-asset-size-app/package.json index 6789683f868..81fe1752474 100644 --- a/tests/full-data-asset-size-app/package.json +++ b/tests/full-data-asset-size-app/package.json @@ -1,6 +1,6 @@ { "name": "full-data-asset-size-app", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "description": "An app for determining asset-size of the meta package", "repository": { diff --git a/tests/main/ember-cli-build.js b/tests/main/ember-cli-build.js index 3cba8b6cfc9..fa7c18ba6c6 100644 --- a/tests/main/ember-cli-build.js +++ b/tests/main/ember-cli-build.js @@ -57,6 +57,9 @@ module.exports = async function (defaults) { setConfig(app, __dirname, { compatWith: process.env.EMBER_DATA_FULL_COMPAT ? '99.0' : null, + deprecations: { + DISABLE_6X_DEPRECATIONS: false, + }, }); return app.toTree(); diff --git a/tests/main/package.json b/tests/main/package.json index 7b4954fad47..f552b9a9232 100644 --- a/tests/main/package.json +++ b/tests/main/package.json @@ -1,6 +1,6 @@ { "name": "main-test-app", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "description": "A data layer for your Ember applications.", "repository": { @@ -108,7 +108,6 @@ "@glint/template": "1.5.0", "@types/qunit": "2.19.10", "@warp-drive/core-types": "workspace:*", - "@warp-drive/schema-record": "workspace:*", "@warp-drive/build-config": "workspace:*", "@warp-drive/holodeck": "workspace:*", "@warp-drive/internal-config": "workspace:*", diff --git a/tests/main/tests/acceptance/relationships/has-many-test.js b/tests/main/tests/acceptance/relationships/has-many-test.js index 4548a68c6d3..7bd2f103bbb 100644 --- a/tests/main/tests/acceptance/relationships/has-many-test.js +++ b/tests/main/tests/acceptance/relationships/has-many-test.js @@ -17,6 +17,8 @@ import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import { LEGACY_SUPPORT } from '@ember-data/model/-private'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; +import { DEPRECATE_ARRAY_LIKE } from '@warp-drive/build-config/deprecations'; class Person extends Model { @attr() @@ -819,7 +821,13 @@ module('autotracking has-many', function (hooks) { } get sortedChildren() { - return this.children.slice().sort((a, b) => (a.name > b.name ? 1 : -1)); + if (DEPRECATE_ARRAY_LIKE) { + const result = this.children.sortBy('name'); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + return result; + } else { + return this.children.slice().sort((a, b) => (a.name > b.name ? 1 : -1)); + } } @action @@ -864,6 +872,219 @@ module('autotracking has-many', function (hooks) { assert.deepEqual(names, ['RGB', 'RGB'], 'rendered 2 children'); }); + deprecatedTest( + 'We can re-render hasMany w/PromiseManyArray.sortBy', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 3 }, + async function (assert) { + class ChildrenList extends Component { + @service store; + + get sortedChildren() { + const result = this.args.person.children.sortBy('name'); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + return result; + } + + @action + createChild() { + const parent = this.args.person; + const name = 'RGB'; + this.store.createRecord('person', { name, parent }); + } + } + + const layout = hbs` + + +

{{this.sortedChildren.length}}

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

{{this.sortedChildren.length}}

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

{{this.firstChild.name}}

+

{{this.lastChild.name}}

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

{{this.children.length}}

+
    + {{#each this.children as |child|}} +
  • {{child.name}}
  • + {{/each}} +
+ `; + this.owner.register('component:children-list', setComponentTemplate(layout, ChildrenList)); + + store.createRecord('person', { id: '1', name: 'Doodad' }); + this.person = store.peekRecord('person', '1'); + + await render(hbs``); + + let names = findAll('li').map((e) => e.textContent); + + assert.deepEqual(names, [], 'rendered no children'); + + await click('#createChild'); + + names = findAll('li').map((e) => e.textContent); + assert.deepEqual(names, ['RGB'], 'rendered 1 child'); + + await click('#createChild'); + + names = findAll('li').map((e) => e.textContent); + assert.deepEqual(names, ['RGB', 'RGB'], 'rendered 2 children'); + } + ); + test('We can re-render hasMany', async function (assert) { class ChildrenList extends Component { @service store; diff --git a/tests/main/tests/acceptance/tracking-promise-flags-test.js b/tests/main/tests/acceptance/tracking-promise-flags-test.js new file mode 100644 index 00000000000..f3ae8ec1117 --- /dev/null +++ b/tests/main/tests/acceptance/tracking-promise-flags-test.js @@ -0,0 +1,69 @@ +import { render, settled } from '@ember/test-helpers'; + +import { module } from 'qunit'; + +import { hbs } from 'ember-cli-htmlbars'; +import { setupRenderingTest } from 'ember-qunit'; + +import JSONAPIAdapter from '@ember-data/adapter/json-api'; +import Model, { attr } from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; + +class Widget extends Model { + @attr name; +} + +module('acceptance/tracking-promise-flags', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + const { owner } = this; + owner.register('model:widget', Widget); + owner.register( + 'serializer:application', + class { + normalizeResponse = (_, __, data) => data; + static create() { + return new this(); + } + } + ); + }); + + deprecatedTest( + 'can track isPending', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 6 }, + async function (assert) { + const { owner } = this; + let resolve; + class TestAdapter extends JSONAPIAdapter { + findRecord() { + return new Promise((r) => { + resolve = r; + }); + } + } + owner.register('adapter:application', TestAdapter); + const store = owner.lookup('service:store'); + store.DISABLE_WAITER = true; + this.model = store.findRecord('widget', '1'); + + await render(hbs`{{#if this.model.isPending}}Pending{{else}}{{this.model.name}}{{/if}}`); + + assert.dom().containsText('Pending'); + + resolve({ + data: { + id: '1', + type: 'widget', + attributes: { + name: 'Contraption', + }, + }, + }); + await settled(); + + assert.dom().containsText('Contraption'); + } + ); +}); diff --git a/tests/main/tests/deprecations/deprecate-early-static-test.js b/tests/main/tests/deprecations/deprecate-early-static-test.js new file mode 100644 index 00000000000..661c9a57b1e --- /dev/null +++ b/tests/main/tests/deprecations/deprecate-early-static-test.js @@ -0,0 +1,63 @@ +import { module } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import Model from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; + +module('Deprecations', function (hooks) { + setupTest(hooks); + + const StaticModelMethods = [ + { name: 'typeForRelationship', count: 3 }, + { name: 'inverseFor', count: 5 }, + { name: '_findInverseFor', count: 3 }, + { name: 'eachRelationship', count: 3 }, + { name: 'eachRelatedType', count: 3 }, + { name: 'determineRelationshipType', count: 1 }, + { name: 'eachAttribute', count: 2 }, + { name: 'eachTransformedAttribute', count: 4 }, + { name: 'toString', count: 1 }, + ]; + const StaticModelGetters = [ + { name: 'inverseMap', count: 1 }, + { name: 'relationships', count: 3 }, + { name: 'relationshipNames', count: 1 }, + { name: 'relatedTypes', count: 2 }, + { name: 'relationshipsByName', count: 2 }, + { name: 'relationshipsObject', count: 1 }, + { name: 'fields', count: 1 }, + { name: 'attributes', count: 1 }, + { name: 'transformedAttributes', count: 3 }, + ]; + + function checkDeprecationForProp(prop) { + deprecatedTest( + `Accessing static prop ${prop.name} is deprecated`, + { id: 'ember-data:deprecate-early-static', until: '5.0', count: prop.count }, + function (assert) { + class Post extends Model {} + Post[prop.name]; + assert.ok(true); + } + ); + } + function checkDeprecationForMethod(method) { + deprecatedTest( + `Accessing static method ${method.name} is deprecated`, + { id: 'ember-data:deprecate-early-static', until: '5.0', count: method.count }, + function (assert) { + class Post extends Model {} + try { + Post[method.name](); + } catch { + // do nothing + } + assert.ok(true); + } + ); + } + + StaticModelGetters.forEach(checkDeprecationForProp); + StaticModelMethods.forEach(checkDeprecationForMethod); +}); diff --git a/tests/main/tests/deprecations/deprecate-helpers-test.js b/tests/main/tests/deprecations/deprecate-helpers-test.js new file mode 100644 index 00000000000..e3b09457598 --- /dev/null +++ b/tests/main/tests/deprecations/deprecate-helpers-test.js @@ -0,0 +1,48 @@ +import { module } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import { errorsArrayToHash, errorsHashToArray } from '@ember-data/adapter/error'; +import { normalizeModelName } from '@ember-data/store'; +import { normalizeModelName as _privateNormalize } from '@ember-data/store/-private'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; + +module('Deprecations', function (hooks) { + setupTest(hooks); + + deprecatedTest( + `Calling normalizeModelName`, + { id: 'ember-data:deprecate-normalize-modelname-helper', until: '5.0', count: 1 }, + function (assert) { + normalizeModelName('user'); + assert.ok(true); + } + ); + + deprecatedTest( + `Calling normalizeModelName imported from private`, + { id: 'ember-data:deprecate-normalize-modelname-helper', until: '5.0', count: 1 }, + function (assert) { + _privateNormalize('user'); + assert.ok(true); + } + ); + + deprecatedTest( + `Calling errorsArrayToHash`, + { id: 'ember-data:deprecate-errors-array-to-hash-helper', until: '5.0', count: 1 }, + function (assert) { + errorsArrayToHash([]); + assert.ok(true); + } + ); + + deprecatedTest( + `Calling errorsHashToArray`, + { id: 'ember-data:deprecate-errors-hash-to-array-helper', until: '5.0', count: 1 }, + function (assert) { + errorsHashToArray({}); + assert.ok(true); + } + ); +}); diff --git a/tests/main/tests/deprecations/deprecate-reopen-class-test.js b/tests/main/tests/deprecations/deprecate-reopen-class-test.js new file mode 100644 index 00000000000..9e4e5dd1f2f --- /dev/null +++ b/tests/main/tests/deprecations/deprecate-reopen-class-test.js @@ -0,0 +1,30 @@ +import { module } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import Model from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; + +module('Deprecations', function (hooks) { + setupTest(hooks); + + deprecatedTest( + `Calling on natively extended class`, + { id: 'ember-data:deprecate-model-reopenclass', until: '5.0', count: 1 }, + function (assert) { + class Post extends Model {} + Post.reopenClass({}); + assert.ok(true); + } + ); + + deprecatedTest( + `Calling on classic extended class`, + { id: 'ember-data:deprecate-model-reopenclass', until: '5.0', count: 1 }, + function (assert) { + const Post = Model.extend(); + Post.reopenClass({}); + assert.ok(true); + } + ); +}); diff --git a/tests/main/tests/deprecations/deprecate-reopen-test.js b/tests/main/tests/deprecations/deprecate-reopen-test.js new file mode 100644 index 00000000000..4cf4ac2c847 --- /dev/null +++ b/tests/main/tests/deprecations/deprecate-reopen-test.js @@ -0,0 +1,30 @@ +import { module } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import Model from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; + +module('Deprecations', function (hooks) { + setupTest(hooks); + + deprecatedTest( + `Calling on natively extended class`, + { id: 'ember-data:deprecate-model-reopen', until: '5.0', count: 1 }, + function (assert) { + class Post extends Model {} + Post.reopen({}); + assert.ok(true); + } + ); + + deprecatedTest( + `Calling on classic extended class`, + { id: 'ember-data:deprecate-model-reopen', until: '5.0', count: 1 }, + function (assert) { + const Post = Model.extend(); + Post.reopen({}); + assert.ok(true); + } + ); +}); diff --git a/tests/main/tests/integration/cache-handler/request-dedupe-test.ts b/tests/main/tests/integration/cache-handler/request-dedupe-test.ts index b1c5b0dde21..08f5d6bae1e 100644 --- a/tests/main/tests/integration/cache-handler/request-dedupe-test.ts +++ b/tests/main/tests/integration/cache-handler/request-dedupe-test.ts @@ -1,52 +1,27 @@ import { module, test } from 'qunit'; -import JSONAPICache from '@ember-data/json-api'; +import Store from 'ember-data/store'; +import { setupTest } from 'ember-qunit'; + +import Model, { attr } from '@ember-data/model'; import type { Handler, RequestContext } from '@ember-data/request'; import RequestManager from '@ember-data/request'; -import Store, { CacheHandler } from '@ember-data/store'; -import type { CacheCapabilitiesManager } from '@ember-data/store/types'; -import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import { CacheHandler } from '@ember-data/store'; import type { SingleResourceDataDocument } from '@warp-drive/core-types/spec/document'; import type { Type } from '@warp-drive/core-types/symbols'; -import { instantiateRecord, teardownRecord } from '@warp-drive/schema-record/hooks'; -import type { SchemaRecord } from '@warp-drive/schema-record/record'; -import { registerDerivations, SchemaService, withDefaults } from '@warp-drive/schema-record/schema'; - -type User = { - id: string; - name: string; - [Type]: 'user'; -}; - -class TestStore extends Store { - createCache(capabilities: CacheCapabilitiesManager) { - return new JSONAPICache(capabilities); - } - - createSchemaService() { - const schema = new SchemaService(); - registerDerivations(schema); - schema.registerResource( - withDefaults({ - type: 'user', - fields: [{ name: 'name', kind: 'field' }], - }) - ); - - return schema; - } - - instantiateRecord(identifier: StableRecordIdentifier, createRecordArgs: { [key: string]: unknown }): unknown { - return instantiateRecord(this, identifier, createRecordArgs); - } - - teardownRecord(record: SchemaRecord) { - teardownRecord(record); - } + +class User extends Model { + @attr declare name: string; + declare [Type]: 'user'; } -module('Integration | Cache Handler | Request Dedupe', function () { +module('Integration | Cache Handler | Request Dedupe', function (hooks) { + setupTest(hooks); + test('it dedupes requests', async function (assert) { + this.owner.register('model:user', User); + this.owner.register('service:store', Store); + const store = this.owner.lookup('service:store') as Store; const TestHandler: Handler = { request(context: RequestContext) { assert.step(`requested: ${context.request.url}`); @@ -61,7 +36,6 @@ module('Integration | Cache Handler | Request Dedupe', function () { } as T); }, }; - const store = new TestStore(); store.requestManager = new RequestManager().use([TestHandler]).useCache(CacheHandler); // trigger simultaneous requests @@ -91,6 +65,9 @@ module('Integration | Cache Handler | Request Dedupe', function () { }); test('it dedupes requests when backgroundReload is used', async function (assert) { + this.owner.register('model:user', User); + this.owner.register('service:store', Store); + const store = this.owner.lookup('service:store') as Store; const TestHandler: Handler = { request(context: RequestContext) { assert.step(`requested: ${context.request.url}`); @@ -105,7 +82,6 @@ module('Integration | Cache Handler | Request Dedupe', function () { } as T); }, }; - const store = new TestStore(); store.requestManager = new RequestManager().use([TestHandler]).useCache(CacheHandler); // trigger simultaneous requests @@ -140,6 +116,9 @@ module('Integration | Cache Handler | Request Dedupe', function () { }); test('it dedupes requests when reload is used', async function (assert) { + this.owner.register('model:user', User); + this.owner.register('service:store', Store); + const store = this.owner.lookup('service:store') as Store; const TestHandler: Handler = { request(context: RequestContext) { assert.step(`requested: ${context.request.url}`); @@ -154,7 +133,6 @@ module('Integration | Cache Handler | Request Dedupe', function () { } as T); }, }; - const store = new TestStore(); store.requestManager = new RequestManager().use([TestHandler]).useCache(CacheHandler); // trigger simultaneous requests @@ -189,6 +167,9 @@ module('Integration | Cache Handler | Request Dedupe', function () { }); test('it dedupes requests when backgroundReload and reload are used', async function (assert) { + this.owner.register('model:user', User); + this.owner.register('service:store', Store); + const store = this.owner.lookup('service:store') as Store; const TestHandler: Handler = { request(context: RequestContext) { assert.step(`requested: ${context.request.url}`); @@ -203,7 +184,6 @@ module('Integration | Cache Handler | Request Dedupe', function () { } as T); }, }; - const store = new TestStore(); store.requestManager = new RequestManager().use([TestHandler]).useCache(CacheHandler); // trigger simultaneous requests @@ -243,6 +223,9 @@ module('Integration | Cache Handler | Request Dedupe', function () { }); test('it dedupes requests when backgroundReload and reload are used (multi-round)', async function (assert) { + this.owner.register('model:user', User); + this.owner.register('service:store', Store); + const store = this.owner.lookup('service:store') as Store; let totalRequests = 0; const TestHandler: Handler = { async request(context: RequestContext) { @@ -259,7 +242,6 @@ module('Integration | Cache Handler | Request Dedupe', function () { } as T); }, }; - const store = new TestStore(); store.requestManager = new RequestManager().use([TestHandler]).useCache(CacheHandler); // trigger simultaneous requests diff --git a/tests/main/tests/integration/emergent-behavior/recovery/belongs-to-test.ts b/tests/main/tests/integration/emergent-behavior/recovery/belongs-to-test.ts new file mode 100644 index 00000000000..398d7dec740 --- /dev/null +++ b/tests/main/tests/integration/emergent-behavior/recovery/belongs-to-test.ts @@ -0,0 +1,301 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import type { Snapshot } from '@ember-data/legacy-compat/-private'; +import Model, { attr, belongsTo } from '@ember-data/model'; +import type Store from '@ember-data/store'; +import type { ModelSchema } from '@ember-data/store/types'; +import { DEBUG } from '@warp-drive/build-config/env'; + +let IS_DEBUG = false; + +if (DEBUG) { + IS_DEBUG = true; +} +class User extends Model { + @attr declare name: string; + @belongsTo('user', { async: false, inverse: null }) declare bestFriend: User; +} + +module('Emergent Behavior > Recovery | belongsTo', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.owner.register('model:user', User); + + const store = this.owner.lookup('service:store') as Store; + store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + bestFriend: { + data: { type: 'user', id: '2' }, + }, + }, + }, + }); + }); + + test('When a sync relationship is accessed before load', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + const bestFriend = user.bestFriend; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.true(bestFriend.isEmpty, 'the relationship is empty'); + assert.strictEqual(bestFriend.id, '2', 'the relationship id is present'); + assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + } + }); + + test('When a sync relationship is accessed before load and later updated remotely', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + user.bestFriend; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + bestFriend: { data: { type: 'user', id: '3' } }, + }, + }, + included: [ + { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + ], + }); + + // access the relationship again + const bestFriend = user.bestFriend; + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, 'Peter', 'the relationship is loaded'); + }); + + test('When a sync relationship is accessed before load and later mutated', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + user.bestFriend; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + + const peter = store.createRecord('user', { name: 'Peter' }) as User; + user.bestFriend = peter; + + // access the relationship again + const bestFriend = user.bestFriend; + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, 'Peter', 'the relationship is loaded'); + }); + + test('When a sync relationship is accessed before load and then later sideloaded', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as User; + + // access the relationship before load + try { + const bestFriend = user.bestFriend; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, undefined, 'the relationship name is not present'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + + // sideload the relationship + store.push({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + + // access the relationship after sideload + try { + const bestFriend = user.bestFriend; + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, 'Krystan', 'the relationship is loaded'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + }); + + test('When a sync relationship is accessed before load and then later attempted to be found via findRecord', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as User; + this.owner.register( + 'adapter:application', + class { + findRecord(_store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + assert.step('findRecord'); + assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); + return Promise.resolve({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + } + static create() { + return new this(); + } + } + ); + + // access the relationship before load + try { + const bestFriend = user.bestFriend; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, undefined, 'the relationship name is not present'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + + // sideload the relationship + try { + await store.findRecord('user', '2'); + assert.notOk(IS_DEBUG, `In production, finding the record should succeed`); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `finding the record should not throw, received ${(e as Error).message}`); + } + assert.verifySteps(['findRecord'], 'we called findRecord'); + + // access the relationship after sideload + try { + const bestFriend = user.bestFriend; + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, 'Krystan', 'the relationship is loaded'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + }); + + test('When a sync relationship is accessed before load and a later attempt to load via findRecord errors', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as User; + this.owner.register( + 'adapter:application', + class { + findRecord(_store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + assert.step('findRecord'); + assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); + + return Promise.reject(new Error('404 - Not Found')); + } + static create() { + return new this(); + } + } + ); + + // access the relationship before load + try { + const bestFriend = user.bestFriend; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(bestFriend.name, undefined, 'the relationship name is not present'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + + // in production because we do not error above the call to getAttr will populate the _attributes object + // in the cache, leading recordData.isEmpty() to return false, thus moving the record into a "loaded" state + // which additionally means that findRecord is treated as a background request. + // + // for this testwe care more that a request is made, than whether it was foreground or background so we force + // the request to be foreground by using reload: true + await store.findRecord('user', '2', { reload: true }).catch(() => { + assert.step('we error'); + }); + assert.verifySteps(['findRecord', 'we error'], 'we called findRecord'); + + // access the relationship after sideload + try { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + user.bestFriend; + + // in production we do not error + assert.ok(true, 'accessing the relationship should not throw'); + } catch (e) { + // In IS_DEBUG we should error + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + } + + try { + const bestFriend = user.bestFriend; + // in IS_DEBUG we should error for this assert + // this is a surprise, because usually failed load attempts result in records being fully removed + // from the store, and so we would expect the relationship to be null + assert.strictEqual(bestFriend.name, undefined, 'the relationship is not loaded'); + } catch (e) { + assert.strictEqual( + (e as Error).message, + `Cannot read properties of null (reading 'name')`, + 'we get the expected error' + ); + } + }); +}); diff --git a/tests/main/tests/integration/emergent-behavior/recovery/has-many-test.ts b/tests/main/tests/integration/emergent-behavior/recovery/has-many-test.ts new file mode 100644 index 00000000000..87c26dffd78 --- /dev/null +++ b/tests/main/tests/integration/emergent-behavior/recovery/has-many-test.ts @@ -0,0 +1,1319 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import type { Snapshot } from '@ember-data/legacy-compat/-private'; +import Model, { attr, type HasMany, hasMany } from '@ember-data/model'; +import type Store from '@ember-data/store'; +import type { ModelSchema } from '@ember-data/store/types'; +import { DEBUG } from '@warp-drive/build-config/env'; +import { Type } from '@warp-drive/core-types/symbols'; + +let IS_DEBUG = false; + +if (DEBUG) { + IS_DEBUG = true; +} +class User extends Model { + declare [Type]: 'user'; + @attr declare name: string; + @hasMany('user', { async: false, inverse: null }) declare friends: HasMany; + @hasMany('user', { async: false, inverse: 'frenemies' }) declare frenemies: HasMany; +} + +module('Emergent Behavior > Recovery | hasMany', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.owner.register('model:user', User); + + const store = this.owner.lookup('service:store') as Store; + store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + friends: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + }); + + test('When a sync relationship is accessed before load', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + const friends = user.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual(store.peekRecord('user', '2'), null, 'the related record is not in the store'); + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + }); + + test('When a sync relationship is accessed before load and later updated remotely', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + const friends = user.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + friends: { data: [{ type: 'user', id: '3' }] }, + }, + }, + included: [ + { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + ], + }); + + // access the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 1, 'the relationship is NOT empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 1, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 1, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 2, 'the store has two records'); + }); + + test('When a sync relationship is accessed before load, records are later loaded, and then it is updated by related record deletion', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + const friends = user.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + const peter = store.push({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + included: [ + { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + { + type: 'user', + id: '4', + attributes: { + name: 'Rey', + }, + }, + ], + }); + + // access the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is still INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 4, 'the store has four records'); + + this.owner.register( + 'adapter:application', + class { + deleteRecord(_store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + return Promise.resolve({ + data: null, + }); + } + static create() { + return new this(); + } + } + ); + + store.deleteRecord(peter); + await store.saveRecord(peter); + store.unloadRecord(peter); + + // access the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 2, 'the relationship state is now correct'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 3, 'the store has three records'); + }); + + test('When a sync relationship is accessed before load and later updated by remote inverse removal', function (assert) { + class LocalUser extends Model { + @attr declare name: string; + @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: HasMany; + } + this.owner.register('model:local-user', LocalUser); + const store = this.owner.lookup('service:store') as Store; + const user1 = store.push({ + data: { + type: 'local-user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + friends: { + data: [ + { type: 'local-user', id: '2' }, + { type: 'local-user', id: '3' }, + { type: 'local-user', id: '4' }, + ], + }, + }, + }, + included: [ + { + type: 'local-user', + id: '4', + attributes: { + name: 'Krystan', + }, + relationships: { + friends: { + data: [{ type: 'local-user', id: '1' }], + }, + }, + }, + ], + }) as LocalUser; + const user2 = store.peekRecord('local-user', '4') as LocalUser; + + assert.strictEqual(user1.name, 'Chris Wagenet', 'precond - user1 is loaded'); + assert.strictEqual(user2.name, 'Krystan', 'precond2 - user is loaded'); + + // access the relationship before load + try { + const friends = user1.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 1, 'the relationship is INCORRECTLY 1'); + assert.strictEqual( + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + + // remove user2 from user1's friends via inverse + store.push({ + data: { + type: 'local-user', + id: '4', + relationships: { + friends: { data: [] }, + }, + }, + }); + + // access the relationship again + try { + const friends = user1.friends; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty and shows length 0 instead of 2'); + assert.strictEqual( + user1.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user1.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + }); + + test('When a sync relationship is accessed before load and later mutated directly', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as User; + + assert.strictEqual(user.name, 'Chris Wagenet', 'precond - user is loaded'); + + // access the relationship before load + try { + const friends = user.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + const peter = store.createRecord('user', { name: 'Peter' }); + + try { + user.friends.push(peter); + assert.notOk(IS_DEBUG, 'mutating the relationship should not throw'); + } catch (e) { + assert.ok(IS_DEBUG, `mutating the relationship should not throw, received ${(e as Error).message}`); + } + + // access the relationship again + try { + const friends = user.friends; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual( + friends.length, + 1, + 'the relationship is NOT empty but INCORRECTLY shows length 1 instead of 4' + ); + assert.strictEqual( + user.hasMany('friends').ids().length, + 4, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 2, 'the store has two records'); + }); + + test('When a sync relationship is accessed before load and later mutated via add by inverse', function (assert) { + class LocalUser extends Model { + declare [Type]: 'local-user'; + @attr declare name: string; + @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: HasMany; + } + this.owner.register('model:local-user', LocalUser); + const store = this.owner.lookup('service:store') as Store; + const user1 = store.push({ + data: { + type: 'local-user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + friends: { + data: [ + { type: 'local-user', id: '2' }, + { type: 'local-user', id: '3' }, + { type: 'local-user', id: '4' }, + ], + }, + }, + }, + included: [ + { + type: 'local-user', + id: '5', + attributes: { + name: 'Krystan', + }, + relationships: { + friends: { + data: [], + }, + }, + }, + ], + }); + const user2 = store.peekRecord('local-user', '5'); + + assert.strictEqual(user1.name, 'Chris Wagenet', 'precond - user1 is loaded'); + assert.strictEqual(user2!.name, 'Krystan', 'precond2 - user is loaded'); + + // access the relationship before load + try { + const friends = user1.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + + // add user2 to user1's friends via inverse + try { + user2!.friends.push(user1); + assert.ok(true, 'mutating the relationship should not throw'); + } catch (e) { + assert.ok(false, `mutating the relationship should not throw, received ${(e as Error).message}`); + } + + // access the relationship again + try { + const friends = user1.friends; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual( + friends.length, + 1, + 'the relationship is NOT empty but INCORRECTLY shows length 1 instead of 4' + ); + assert.strictEqual( + user1.hasMany('friends').ids().length, + 4, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user1.hasMany('friends').ids().length, + 4, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + }); + + test('When a sync relationship is accessed before load and later mutated via remove by inverse', function (assert) { + class LocalUser extends Model { + declare [Type]: 'local-user'; + @attr declare name: string; + @hasMany('local-user', { async: false, inverse: 'friends' }) declare friends: HasMany; + } + this.owner.register('model:local-user', LocalUser); + const store = this.owner.lookup('service:store') as Store; + const user1 = store.push({ + data: { + type: 'local-user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + friends: { + data: [ + { type: 'local-user', id: '2' }, + { type: 'local-user', id: '3' }, + { type: 'local-user', id: '4' }, + ], + }, + }, + }, + included: [ + { + type: 'local-user', + id: '4', + attributes: { + name: 'Krystan', + }, + relationships: { + friends: { + data: [{ type: 'local-user', id: '1' }], + }, + }, + }, + ], + }); + const user2 = store.peekRecord('local-user', '4')!; + + assert.strictEqual(user1.name, 'Chris Wagenet', 'precond - user1 is loaded'); + assert.strictEqual(user2.name, 'Krystan', 'precond2 - user is loaded'); + + // access the relationship before load + try { + const friends = user1.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 1, 'the relationship is INCORRECTLY 1'); + + assert.strictEqual( + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + + assert.strictEqual( + user1.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + + // remove user2 from user1's friends via inverse + try { + const index = user2.friends.indexOf(user1); + user2.friends.splice(index, 1); + assert.ok(true, 'mutating the relationship should not throw'); + } catch (e) { + assert.ok(false, `mutating the relationship should not throw, received ${(e as Error).message}`); + } + + // access the relationship again + try { + const friends = user1.friends; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty and shows length 0 instead of 2'); + + assert.strictEqual( + user1.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + + assert.strictEqual( + user1.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('local-user').length, 2, 'the store has two records'); + }); + + test('When a sync relationship is accessed before load and then later sideloaded', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as User; + + // access the relationship before load + try { + const friends = user.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + // sideload the relationships + store.push({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + store.push({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + }); + store.push({ + data: { + type: 'user', + id: '4', + attributes: { + name: 'Rey', + }, + }, + }); + + // access the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 4, 'the store has four records'); + + // attempt notify of the relationship + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + friends: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + + // access the relationship + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + }); + + test('When a sync relationship is accessed before load and then later one of the missing records is attempted to be found via findRecord (inverse: null)', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as User; + this.owner.register( + 'adapter:application', + class { + findRecord(_store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + assert.step('findRecord'); + assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); + return Promise.resolve({ + data: { + type: 'user', + id: '4', + attributes: { + name: 'Rey', + }, + }, + }); + } + static create() { + return new this(); + } + } + ); + + // access the relationship before load + try { + const friends = user.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + // sideload two of the relationships + store.push({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + store.push({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + }); + + // access the relationship again + try { + const friends = user.friends; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 3, 'the store has four records'); + + // attempt notify of the relationship + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + friends: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + + // access the relationship + try { + const friends = user.friends; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + // attempt to find the missing record + try { + await store.findRecord('user', '4'); + assert.notOk(IS_DEBUG, 'finding the missing record should not throw'); + } catch (e) { + assert.ok(IS_DEBUG, `finding the missing record should not throw, received ${(e as Error).message}`); + } + assert.verifySteps(['findRecord'], 'we called findRecord'); + + // check the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + + // in debug since we error and the error is caught (in the tests) + // we remove the record from the cache and enter an accessible state + // in which length is 2 + assert.strictEqual( + friends.length, + IS_DEBUG ? 2 : 0, + 'the relationship is INCORRECTLY emptied, INCORRECTLY 2 if in debug' + ); + assert.strictEqual( + user.hasMany('friends').ids().length, + IS_DEBUG ? 2 : 3, + 'the relationship reference contains the expected ids (3), INCORRECTLY 2 if in debug' + ); + assert.strictEqual( + store.peekAll('user').length, + IS_DEBUG ? 3 : 4, + 'the store correctly shows 4 records (3 if debug since we error)' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + assert.strictEqual(store.peekAll('user').length, 4, 'the store correctly shows 4 records'); + } + }); + + test('When a sync relationship is accessed before load and then later one of the missing records is attempted to be found via findRecord (inverse: specified)', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as User; + store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris Wagenet', + }, + relationships: { + frenemies: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + this.owner.register( + 'adapter:application', + class { + findRecord(_store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + assert.step('findRecord'); + if (snapshot.include === 'frenemies') { + assert.deepEqual(snapshot._attributes, { name: 'Rey' }, 'the snapshot has the correct attributes'); + + return Promise.resolve({ + data: { + type: 'user', + id: '4', + attributes: { + name: 'Rey', + }, + relationships: { + frenemies: { + data: [{ type: 'user', id: '1' }], + }, + }, + }, + }); + } + assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); + + return Promise.resolve({ + data: { + type: 'user', + id: '4', + attributes: { + name: 'Rey', + }, + }, + }); + } + static create() { + return new this(); + } + } + ); + + // access the relationship before load + try { + const friends = user.frenemies; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + // sideload two of the relationships + store.push({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + store.push({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + }); + + // access the relationship again + try { + const friends = user.frenemies; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 3, 'the store has three records'); + + // attempt notify of the relationship + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + frenemies: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + + // access the relationship + try { + const friends = user.frenemies; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY length 0 instead of 3'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + // attempt to find the missing record + try { + await store.findRecord('user', '4'); + assert.notOk(IS_DEBUG, 'finding the missing record should not throw'); + } catch (e) { + assert.ok(IS_DEBUG, `finding the missing record should not throw, received ${(e as Error).message}`); + } + assert.verifySteps(['findRecord'], 'we called findRecord'); + + // check the relationship again + try { + const friends = user.frenemies; + + assert.ok(true, 'accessing the relationship should not throw'); + // in debug since we error and the error is caught (in the tests) + // we remove the record from the cache and enter an accessible state + // in which length is 2 + assert.strictEqual( + friends.length, + IS_DEBUG ? 2 : 0, + 'the relationship is INCORRECTLY emptied, INCORRECTLY 2 if in debug' + ); + assert.strictEqual( + user.hasMany('friends').ids().length, + IS_DEBUG ? 2 : 3, + 'the relationship reference contains the expected ids (3), INCORRECTLY 2 if in debug' + ); + assert.strictEqual( + store.peekAll('user').length, + IS_DEBUG ? 3 : 4, + 'the store correctly shows 4 records (3 if we are a debug build since we error)' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + assert.strictEqual(store.peekAll('user').length, 3, 'the store INCORRECTLY shows 3 instead of 4 records'); + } + + // attempt to find the missing record with sideload + try { + await store.findRecord('user', '4', { reload: true, include: 'frenemies' }); + assert.notOk(IS_DEBUG, 'finding the missing record should not throw'); + } catch (e) { + assert.ok(IS_DEBUG, `finding the missing record should not throw, received ${(e as Error).message}`); + } + assert.verifySteps(['findRecord'], 'we called findRecord'); + + // check the relationship again + try { + const friends = user.frenemies; + + assert.ok(true, 'accessing the relationship should not throw'); + + // in debug since we error and the error is caught (in the tests) + // we remove the record from the cache and enter an accessible state + // in which length is 2 + assert.strictEqual( + friends.length, + IS_DEBUG ? 2 : 0, + 'the relationship is INCORRECTLY emptied, INCORRECTLY 2 if in debug' + ); + assert.strictEqual( + user.hasMany('friends').ids().length, + IS_DEBUG ? 2 : 3, + 'the relationship reference contains the expected ids (3), INCORRECTLY 2 if in debug' + ); + assert.strictEqual( + store.peekAll('user').length, + IS_DEBUG ? 3 : 4, + 'the store correctly shows 4 records (3 if we are a debug build since we error)' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual(store.peekAll('user').length, 3, 'the store INCORRECTLY shows 3 instead of 4 records'); + } + }); + + test('When a sync relationship is accessed before load and then later when one of the missing records is later attempt to load via findRecord would error (inverse: null)', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const user = store.peekRecord('user', '1') as User; + this.owner.register( + 'adapter:application', + class { + findRecord(_store: Store, schema: ModelSchema, id: string, snapshot: Snapshot) { + assert.step('findRecord'); + assert.deepEqual(snapshot._attributes, { name: undefined }, 'the snapshot has the correct attributes'); + + return Promise.reject(new Error('404 - Not Found')); + } + static create() { + return new this(); + } + } + ); + + // access the relationship before load + try { + const friends = user.friends; + + // in IS_DEBUG we error and should not reach here + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + // In IS_DEBUG we should reach here, in production we should not + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 1, 'the store has only one record'); + + // sideload two of the relationships + store.push({ + data: { + type: 'user', + id: '2', + attributes: { + name: 'Krystan', + }, + }, + }); + store.push({ + data: { + type: 'user', + id: '3', + attributes: { + name: 'Peter', + }, + }, + }); + + // access the relationship again + try { + const friends = user.friends; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + assert.strictEqual(store.peekAll('user').length, 3, 'the store has four records'); + + // attempt notify of the relationship + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + friends: { + data: [ + { type: 'user', id: '2' }, + { type: 'user', id: '3' }, + { type: 'user', id: '4' }, + ], + }, + }, + }, + }); + + // access the relationship + try { + const friends = user.friends; + + assert.notOk(IS_DEBUG, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 0, 'the relationship is INCORRECTLY empty'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(IS_DEBUG, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 3, + 'the relationship reference contains the expected ids' + ); + } + + // attempt to find the missing record + try { + await store.findRecord('user', '4'); + assert.ok(false, 'finding the missing record should throw'); + } catch (e) { + assert.ok(true, `finding the missing record should throw, received ${(e as Error).message}`); + } + assert.verifySteps(['findRecord'], 'we called findRecord'); + + // check the relationship again + try { + const friends = user.friends; + + assert.ok(true, 'accessing the relationship should not throw'); + assert.strictEqual(friends.length, 2, 'the relationship is correct'); + assert.strictEqual( + user.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } catch (e) { + assert.ok(false, `accessing the relationship should not throw, received ${(e as Error).message}`); + assert.strictEqual( + user.hasMany('friends').ids().length, + 2, + 'the relationship reference contains the expected ids' + ); + } + }); +}); diff --git a/tests/main/tests/integration/inverse-test.js b/tests/main/tests/integration/inverse-test.js new file mode 100644 index 00000000000..217ff318a38 --- /dev/null +++ b/tests/main/tests/integration/inverse-test.js @@ -0,0 +1,312 @@ +import { module } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import Model, { attr, belongsTo } from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; +import { DEBUG } from '@warp-drive/build-config/env'; + +function stringify(string) { + return function () { + return string; + }; +} + +module('integration/inverse-test - inverseFor', function (hooks) { + setupTest(hooks); + let store; + + hooks.beforeEach(function () { + const { owner } = this; + store = owner.lookup('service:store'); + }); + + deprecatedTest( + 'Finds the inverse when there is only one possible available', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + function (assert) { + class User extends Model { + @attr() + name; + + @belongsTo('user', { async: true, inverse: null }) + bestFriend; + + @belongsTo('job', { async: false }) + job; + + toString() { + return stringify('user'); + } + } + + class Job extends Model { + @attr() + isGood; + + @belongsTo('user', { async: false }) + user; + + toString() { + return stringify('job'); + } + } + + const { owner } = this; + owner.register('model:user', User); + owner.register('model:job', Job); + + const job = store.modelFor('job'); + const inverseDefinition = job.inverseFor('user', store); + + assert.deepEqual( + inverseDefinition, + { + type: 'user', + name: 'job', + kind: 'belongsTo', + options: { + async: false, + }, + }, + 'Gets correct type, name and kind' + ); + } + ); + + deprecatedTest( + 'Finds the inverse when only one side has defined it manually', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 3 }, + function (assert) { + class User extends Model { + @attr() + name; + + @belongsTo('user', { async: true, inverse: null }) + bestFriend; + + @belongsTo('job', { async: false }) + job; + + @belongsTo('job', { async: false }) + previousJob; + + toString() { + return stringify('user'); + } + } + + class Job extends Model { + @attr() + isGood; + + @belongsTo('user', { async: false }) + user; + + @belongsTo('user', { inverse: 'previousJob', async: false }) + owner; + + toString() { + return stringify('job'); + } + } + + const { owner } = this; + owner.register('model:user', User); + owner.register('model:job', Job); + + const job = store.modelFor('job'); + const user = store.modelFor('user'); + + assert.deepEqual( + job.inverseFor('owner', store), + { + type: 'user', //the model's type + name: 'previousJob', //the models relationship key + kind: 'belongsTo', + options: { + async: false, + }, + }, + 'Gets correct type, name and kind' + ); + + assert.deepEqual( + user.inverseFor('previousJob', store), + { + type: 'job', //the model's type + name: 'owner', //the models relationship key + kind: 'belongsTo', + options: { + inverse: 'previousJob', + async: false, + }, + }, + 'Gets correct type, name and kind' + ); + } + ); + + deprecatedTest( + 'Returns null if inverse relationship it is manually set with a different relationship key', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 1 }, + function (assert) { + class User extends Model { + @attr() + name; + + @belongsTo('user', { async: true, inverse: null }) + bestFriend; + + @belongsTo('job', { async: false }) + job; + + toString() { + return stringify('user'); + } + } + + class Job extends Model { + @attr() + isGood; + + @belongsTo('user', { inverse: 'previousJob', async: false }) + user; + + toString() { + return stringify('job'); + } + } + + const { owner } = this; + owner.register('model:user', User); + owner.register('model:job', Job); + + const user = store.modelFor('user'); + assert.strictEqual(user.inverseFor('job', store), null, 'There is no inverse'); + } + ); + + if (DEBUG) { + deprecatedTest( + 'Errors out if you define 2 inverses to the same model', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 1 }, + function (assert) { + class User extends Model { + @attr() + name; + + @belongsTo('user', { async: true, inverse: null }) + bestFriend; + + @belongsTo('job', { async: false }) + job; + + toString() { + return stringify('user'); + } + } + + class Job extends Model { + @attr() + isGood; + + @belongsTo('user', { inverse: 'job', async: false }) + user; + + @belongsTo('user', { inverse: 'job', async: false }) + owner; + + toString() { + return stringify('job'); + } + } + const { owner } = this; + owner.register('model:user', User); + owner.register('model:job', Job); + + const user = store.modelFor('user'); + assert.expectAssertion(() => { + user.inverseFor('job', store); + }, /You defined the 'job' relationship on model:user, but you defined the inverse relationships of type model:job multiple times/i); + } + ); + } + + deprecatedTest( + 'Caches findInverseFor return value', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + function (assert) { + assert.expect(1); + class User extends Model { + @attr() + name; + + @belongsTo('user', { async: true, inverse: null }) + bestFriend; + + @belongsTo('job', { async: false }) + job; + + toString() { + return stringify('user'); + } + } + + class Job extends Model { + @attr() + isGood; + + @belongsTo('user', { async: false }) + user; + + toString() { + return stringify('job'); + } + } + const { owner } = this; + owner.register('model:user', User); + owner.register('model:job', Job); + + const job = store.modelFor('job'); + + const inverseForUser = job.inverseFor('user', store); + job.findInverseFor = function () { + assert.ok(false, 'Find is not called anymore'); + }; + + assert.strictEqual(inverseForUser, job.inverseFor('user', store), 'Inverse cached succesfully'); + } + ); + + if (DEBUG) { + deprecatedTest( + 'Errors out if you do not define an inverse for a reflexive relationship', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 1 }, + function (assert) { + class ReflexiveModel extends Model { + @belongsTo('reflexive-model', { async: false }) + reflexiveProp; + + toString() { + return stringify('reflexiveModel'); + } + } + + const { owner } = this; + owner.register('model:reflexive-model', ReflexiveModel); + + //Maybe store is evaluated lazily, so we need this :( + assert.expectWarning(() => { + const reflexiveModel = store.push({ + data: { + type: 'reflexive-model', + id: '1', + }, + }); + reflexiveModel.reflexiveProp; + }, /Detected a reflexive relationship named 'reflexiveProp' on the schema for 'reflexive-model' without an inverse option/); + } + ); + } +}); diff --git a/tests/main/tests/integration/record-arrays/adapter-populated-record-array-test.js b/tests/main/tests/integration/record-arrays/adapter-populated-record-array-test.js index fda58f2a4b6..fa30ecd99c4 100644 --- a/tests/main/tests/integration/record-arrays/adapter-populated-record-array-test.js +++ b/tests/main/tests/integration/record-arrays/adapter-populated-record-array-test.js @@ -167,6 +167,22 @@ module('integration/record-arrays/collection', function (hooks) { ); }); + test('recordArray.replace() throws error', async function (assert) { + const store = this.owner.lookup('service:store'); + const recordArray = store.recordArrayManager.createArray({ type: 'person', query: null }); + + await settled(); + + assert.expectAssertion( + () => { + recordArray.replace(); + }, + 'Mutating this array of records via splice is not allowed.', + 'throws error' + ); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + }); + test('recordArray mutation throws error', async function (assert) { const store = this.owner.lookup('service:store'); const recordArray = store.recordArrayManager.createArray({ type: 'person', query: null }); diff --git a/tests/main/tests/integration/records/relationship-changes-test.js b/tests/main/tests/integration/records/relationship-changes-test.js index ca899a4f49c..c6f9ebcd1fc 100644 --- a/tests/main/tests/integration/records/relationship-changes-test.js +++ b/tests/main/tests/integration/records/relationship-changes-test.js @@ -1,3 +1,5 @@ +import EmberObject from '@ember/object'; +import { alias } from '@ember/object/computed'; import { settled } from '@ember/test-helpers'; import { module, test } from 'qunit'; @@ -7,6 +9,7 @@ import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; const Author = Model.extend({ name: attr('string'), @@ -62,6 +65,114 @@ module('integration/records/relationship-changes - Relationship changes', functi this.owner.register('serializer:application', class extends JSONAPISerializer {}); }); + deprecatedTest( + 'Calling push with relationship recalculates computed alias property if the relationship was empty and is added to', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 2 }, + function (assert) { + assert.expect(2); + + const store = this.owner.lookup('service:store'); + + const Obj = EmberObject.extend({ + person: null, + siblings: alias('person.siblings'), + }); + + const obj = Obj.create(); + + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: { + firstName: 'Yehuda', + lastName: 'Katz', + }, + relationships: { + siblings: { + data: [], + }, + }, + }, + }); + obj.person = store.peekRecord('person', 'wat'); + assert.arrayStrictEquals(obj.siblings.slice(), [], 'siblings cp should have calculated empty initially'); + + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: {}, + relationships: { + siblings: { + data: [sibling1Ref], + }, + }, + }, + included: [sibling1], + }); + + const cpResult = obj.siblings.slice(); + assert.strictEqual(cpResult.length, 1, 'siblings cp should have recalculated'); + obj.destroy(); + } + ); + + deprecatedTest( + 'Calling push with relationship recalculates computed alias property to firstObject if the relationship was empty and is added to', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 1 }, + function (assert) { + assert.expect(3); + + const store = this.owner.lookup('service:store'); + + const Obj = EmberObject.extend({ + person: null, + firstSibling: alias('person.siblings.firstObject'), + }); + + const obj = Obj.create(); + + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: { + firstName: 'Yehuda', + lastName: 'Katz', + }, + relationships: { + siblings: { + data: [], + }, + }, + }, + }); + obj.person = store.peekRecord('person', 'wat'); + assert.strictEqual(obj.sibling, undefined, 'We have no first sibling initially'); + + store.push({ + data: { + type: 'person', + id: 'wat', + attributes: {}, + relationships: { + siblings: { + data: [sibling1Ref], + }, + }, + }, + included: [sibling1], + }); + + const cpResult = obj.firstSibling; + assert.strictEqual(cpResult?.id, '1', 'siblings cp should have recalculated'); + obj.destroy(); + + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + } + ); + test('Calling push with relationship triggers observers once if the relationship was not empty and was added to', async function (assert) { assert.expect(2); diff --git a/tests/main/tests/integration/records/save-test.js b/tests/main/tests/integration/records/save-test.js index ddb5a31555d..762ce8e5527 100644 --- a/tests/main/tests/integration/records/save-test.js +++ b/tests/main/tests/integration/records/save-test.js @@ -10,6 +10,7 @@ import Model, { attr } from '@ember-data/model'; import { createDeferred } from '@ember-data/request'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_SAVE_PROMISE_ACCESS } from '@warp-drive/build-config/deprecations'; module('integration/records/save - Save Record', function (hooks) { setupTest(hooks); @@ -36,12 +37,35 @@ module('integration/records/save - Save Record', function (hooks) { const saved = post.save(); + if (DEPRECATE_SAVE_PROMISE_ACCESS) { + // `save` returns a PromiseObject which allows to call get on it + assert.strictEqual(saved.get('id'), undefined, `.get('id') is undefined before save resolves`); + } + deferred.resolve({ data: { id: '123', type: 'post' } }); const model = await saved; assert.ok(true, 'save operation was resolved'); - assert.strictEqual(saved.id, undefined, `.id is undefined after save resolves`); - assert.strictEqual(model.id, '123', `record.id is '123' after save resolves`); + if (DEPRECATE_SAVE_PROMISE_ACCESS) { + assert.strictEqual(saved.get('id'), '123', `.get('id') is '123' after save resolves`); + assert.strictEqual(model.id, '123', `record.id is '123' after save resolves`); + } else { + assert.strictEqual(saved.id, undefined, `.id is undefined after save resolves`); + assert.strictEqual(model.id, '123', `record.id is '123' after save resolves`); + } assert.strictEqual(model, post, 'resolves with the model'); + if (DEPRECATE_SAVE_PROMISE_ACCESS) { + // We don't care about the exact value of the property, but accessing it + // should not throw an error and only show a deprecation. + saved.__ec_cancel__ = true; + assert.true(saved.__ec_cancel__, '__ec_cancel__ can be accessed on the proxy'); + assert.strictEqual( + model.__ec_cancel__, + undefined, + '__ec_cancel__ can be accessed on the record but is not present' + ); + + assert.expectDeprecation({ id: 'ember-data:model-save-promise', count: 10 }); + } }); test('Will reject save on error', async function (assert) { diff --git a/tests/main/tests/integration/references/belongs-to-test.js b/tests/main/tests/integration/references/belongs-to-test.js index 6c8f5ccf264..bed38c7385c 100644 --- a/tests/main/tests/integration/references/belongs-to-test.js +++ b/tests/main/tests/integration/references/belongs-to-test.js @@ -6,8 +6,11 @@ import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { createDeferred } from '@ember-data/request'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@warp-drive/build-config/deprecations'; class Family extends Model { @hasMany('person', { async: true, inverse: 'family' }) persons; @@ -409,6 +412,48 @@ module('integration/references/belongs-to', function (hooks) { assert.deepEqual(familyReference.meta(), { updatedAt: timestamp2 }, 'meta is updated'); }); + deprecatedTest( + 'push(promise)', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 1 }, + async function (assert) { + const store = this.owner.lookup('service:store'); + const Family = store.modelFor('family'); + + const deferred = createDeferred(); + + const person = store.push({ + data: { + type: 'person', + id: '1', + relationships: { + family: { + data: { type: 'family', id: '1' }, + }, + }, + }, + }); + const familyReference = person.belongsTo('family'); + const push = familyReference.push(deferred.promise); + + assert.ok(push.then, 'BelongsToReference.push returns a promise'); + + deferred.resolve({ + data: { + type: 'family', + id: '1', + attributes: { + name: 'Coreleone', + }, + }, + }); + + await push.then(function (record) { + assert.ok(record instanceof Family, 'push resolves with the record'); + assert.strictEqual(record.name, 'Coreleone', 'name is updated'); + }); + } + ); + testInDebug('push(object) asserts for invalid modelClass', async function (assert) { class Family extends Model { @hasMany('person', { async: true, inverse: 'family' }) persons; @@ -444,9 +489,14 @@ module('integration/references/belongs-to', function (hooks) { const familyReference = person.belongsTo('family'); - await assert.expectAssertion(async function () { - await familyReference.push(anotherPerson); - }, "The 'person' type does not implement 'family' and thus cannot be assigned to the 'family' relationship in 'person'. If this relationship should be polymorphic, mark person.family as `polymorphic: true` and person.persons as implementing it via `as: 'family'`."); + await assert.expectAssertion( + async function () { + await familyReference.push(anotherPerson); + }, + DEPRECATE_NON_EXPLICIT_POLYMORPHISM + ? "The 'person' type does not implement 'family' and thus cannot be assigned to the 'family' relationship in 'person'. Make it a descendant of 'family' or use a mixin of the same name." + : "The 'person' type does not implement 'family' and thus cannot be assigned to the 'family' relationship in 'person'. If this relationship should be polymorphic, mark person.family as `polymorphic: true` and person.persons as implementing it via `as: 'family'`." + ); }); testInDebug('push(object) works with polymorphic types', async function (assert) { diff --git a/tests/main/tests/integration/references/has-many-test.js b/tests/main/tests/integration/references/has-many-test.js index 260f15b4b0a..eb1b8561e9b 100755 --- a/tests/main/tests/integration/references/has-many-test.js +++ b/tests/main/tests/integration/references/has-many-test.js @@ -4,8 +4,11 @@ import { setupRenderingTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { createDeferred } from '@ember-data/request'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@warp-drive/build-config/deprecations'; import createTrackingContext from '../../helpers/create-tracking-context'; @@ -329,9 +332,14 @@ module('integration/references/has-many', function (hooks) { }); const petsReference = person.hasMany('pets'); - await assert.expectAssertion(async () => { - await petsReference.push([{ type: 'person', id: '1' }]); - }, "The 'person' type does not implement 'animal' and thus cannot be assigned to the 'pets' relationship in 'person'. If this relationship should be polymorphic, mark person.pets as `polymorphic: true` and person.owner as implementing it via `as: 'animal'`."); + await assert.expectAssertion( + async () => { + await petsReference.push([{ type: 'person', id: '1' }]); + }, + DEPRECATE_NON_EXPLICIT_POLYMORPHISM + ? "The 'person' type does not implement 'animal' and thus cannot be assigned to the 'pets' relationship in 'person'. Make it a descendant of 'animal' or use a mixin of the same name." + : "The 'person' type does not implement 'animal' and thus cannot be assigned to the 'pets' relationship in 'person'. If this relationship should be polymorphic, mark person.pets as `polymorphic: true` and person.owner as implementing it via `as: 'animal'`." + ); }); test('push valid json:api', async function (assert) { @@ -375,6 +383,48 @@ module('integration/references/has-many', function (hooks) { assert.strictEqual(personsReference.link(), '/families/1/persons', 'link is not updated'); }); + deprecatedTest( + 'push(promise)', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 1 }, + async function (assert) { + const store = this.owner.lookup('service:store'); + const deferred = createDeferred(); + + const family = store.push({ + data: { + type: 'family', + id: '1', + relationships: { + persons: { + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + ], + }, + }, + }, + }); + const personsReference = family.hasMany('persons'); + const pushResult = personsReference.push(deferred.promise); + + assert.ok(pushResult.then, 'HasManyReference.push returns a promise'); + + const payload = { + data: [ + { type: 'person', id: '1', attributes: { name: 'Vito' } }, + { type: 'person', id: '2', attributes: { name: 'Michael' } }, + ], + }; + + deferred.resolve(payload); + + const records = await pushResult; + assert.strictEqual(records.length, 2); + assert.strictEqual(records.at(0).name, 'Vito'); + assert.strictEqual(records.at(1).name, 'Michael'); + } + ); + test('push(document) can update links', async function (assert) { const store = this.owner.lookup('service:store'); diff --git a/tests/main/tests/integration/relationships/belongs-to-test.js b/tests/main/tests/integration/relationships/belongs-to-test.js index 8b945fbb8d6..df839881df4 100644 --- a/tests/main/tests/integration/relationships/belongs-to-test.js +++ b/tests/main/tests/integration/relationships/belongs-to-test.js @@ -7,7 +7,9 @@ import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@warp-drive/build-config/deprecations'; import { getRelationshipStateForRecord, hasRelationshipForRecord } from '../../helpers/accessors'; @@ -580,9 +582,14 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }, }); - assert.expectAssertion(() => { - post.user = comment; - }, "The 'comment' type does not implement 'user' and thus cannot be assigned to the 'user' relationship in 'post'. If this relationship should be polymorphic, mark message.user as `polymorphic: true` and comment.messages as implementing it via `as: 'user'`."); + assert.expectAssertion( + () => { + post.user = comment; + }, + DEPRECATE_NON_EXPLICIT_POLYMORPHISM + ? "The 'comment' type does not implement 'user' and thus cannot be assigned to the 'user' relationship in 'post'. Make it a descendant of 'user' or use a mixin of the same name." + : "The 'comment' type does not implement 'user' and thus cannot be assigned to the 'user' relationship in 'post'. If this relationship should be polymorphic, mark message.user as `polymorphic: true` and comment.messages as implementing it via `as: 'user'`." + ); } ); @@ -825,6 +832,52 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function }); }); + deprecatedTest( + 'A record can be created with a resolved belongsTo promise', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0' }, + async function (assert) { + assert.expect(1); + + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + adapter.shouldBackgroundReloadRecord = () => false; + + const Group = Model.extend({ + people: hasMany('person', { async: false, inverse: 'group' }), + }); + + const Person = Model.extend({ + group: belongsTo('group', { async: true, inverse: 'people' }), + }); + + this.owner.register('model:group', Group); + this.owner.register('model:person', Person); + + store.push({ + data: { + id: '1', + type: 'group', + }, + }); + const originalOwner = store.push({ + data: { + id: '1', + type: 'person', + group: { data: { type: 'group', id: '1' } }, + }, + }); + + const groupPromise = originalOwner.group; + const group = await groupPromise; + const person = store.createRecord('person', { + group: groupPromise, + }); + const personGroup = await person.group; + assert.strictEqual(personGroup, group, 'the group matches'); + } + ); + test('polymorphic belongsTo class-checks check the superclass', function (assert) { assert.expect(1); @@ -1097,6 +1150,19 @@ module('integration/relationship/belongs_to Belongs-To Relationships', function assert.strictEqual(book.author, author, 'Book has an author after rollback attributes'); }); + testInDebug('Passing a model as type to belongsTo should not work', function (assert) { + assert.expect(2); + + assert.expectAssertion(() => { + const User = Model.extend(); + + Model.extend({ + user: belongsTo(User, { async: false, inverse: null }), + }); + }, /The first argument to belongsTo must be a string/); + assert.expectDeprecation({ id: 'ember-data:deprecate-non-strict-relationships' }); + }); + test('belongsTo hasAnyRelationshipData async loaded', async function (assert) { assert.expect(1); class Book extends Model { diff --git a/tests/main/tests/integration/relationships/collection/mutating-has-many-test.ts b/tests/main/tests/integration/relationships/collection/mutating-has-many-test.ts index 169ed675f40..6037969b471 100644 --- a/tests/main/tests/integration/relationships/collection/mutating-has-many-test.ts +++ b/tests/main/tests/integration/relationships/collection/mutating-has-many-test.ts @@ -8,19 +8,13 @@ import type { ManyArray } from '@ember-data/model'; import Model, { attr, hasMany } from '@ember-data/model'; import type Store from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store'; -import { DEPRECATE_MANY_ARRAY_DUPLICATES } from '@warp-drive/build-config/deprecations'; +import { DEPRECATE_MANY_ARRAY_DUPLICATES, DISABLE_6X_DEPRECATIONS } from '@warp-drive/build-config/deprecations'; import type { ExistingResourceIdentifierObject } from '@warp-drive/core-types/spec/json-api-raw'; import { Type } from '@warp-drive/core-types/symbols'; import type { ReactiveContext } from '../../../helpers/reactive-context'; import { reactiveContext } from '../../../helpers/reactive-context'; -let IS_DEPRECATE_MANY_ARRAY_DUPLICATES = false; - -if (DEPRECATE_MANY_ARRAY_DUPLICATES) { - IS_DEPRECATE_MANY_ARRAY_DUPLICATES = true; -} - class User extends Model { @attr declare name: string; @hasMany('user', { async: false, inverse: 'friends' }) declare friends: ManyArray; @@ -214,8 +208,11 @@ async function applyMutation(assert: Assert, store: Store, record: User, mutatio const result = generateAppliedMutation(store, record, mutation); const initialIds = record.friends.map((f) => f.id).join(','); - const shouldError = result.hasDuplicates && !IS_DEPRECATE_MANY_ARRAY_DUPLICATES; - const shouldDeprecate = result.hasDuplicates && IS_DEPRECATE_MANY_ARRAY_DUPLICATES; + const shouldError = result.hasDuplicates && /* inline-macro-config */ !DEPRECATE_MANY_ARRAY_DUPLICATES; + const shouldDeprecate = + result.hasDuplicates && + /* inline-macro-config */ DEPRECATE_MANY_ARRAY_DUPLICATES && + /* inline-macro-config */ !DISABLE_6X_DEPRECATIONS; const expected = shouldError ? result.unchanged : result.deduped; try { diff --git a/tests/main/tests/integration/relationships/has-many-test.js b/tests/main/tests/integration/relationships/has-many-test.js index a6a9326c048..85e02673651 100644 --- a/tests/main/tests/integration/relationships/has-many-test.js +++ b/tests/main/tests/integration/relationships/has-many-test.js @@ -12,6 +12,7 @@ import JSONAPISerializer from '@ember-data/serializer/json-api'; import RESTSerializer from '@ember-data/serializer/rest'; import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_ARRAY_LIKE, DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@warp-drive/build-config/deprecations'; import { getRelationshipStateForRecord, hasRelationshipForRecord } from '../../helpers/accessors'; @@ -1403,6 +1404,59 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( }); }); + deprecatedTest( + 'PromiseArray proxies createRecord to its ManyArray once the hasMany is loaded', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 1 }, + async function (assert) { + assert.expect(4); + class Post extends Model { + @attr title; + @hasMany('comment', { async: true, inverse: 'message' }) comments; + } + + class Comment extends Model { + @attr body; + @belongsTo('post', { async: false, inverse: 'comments' }) message; + } + this.owner.register('model:post', Post); + this.owner.register('model:comment', Comment); + + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + adapter.findHasMany = function (store, snapshot, link, relationship) { + return Promise.resolve({ + data: [ + { id: '1', type: 'comment', attributes: { body: 'First' } }, + { id: '2', type: 'comment', attributes: { body: 'Second' } }, + ], + }); + }; + const post = store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + links: { + related: 'someLink', + }, + }, + }, + }, + }); + + await post.comments.then(function (comments) { + assert.true(comments.isLoaded, 'comments are loaded'); + assert.strictEqual(comments.length, 2, 'comments have 2 length'); + + const newComment = post.comments.createRecord({ body: 'Third' }); + assert.strictEqual(newComment.body, 'Third', 'new comment is returned'); + assert.strictEqual(comments.length, 3, 'comments have 3 length, including new record'); + }); + } + ); + test('An updated `links` value should invalidate a relationship cache', async function (assert) { assert.expect(8); class Post extends Model { @@ -1585,6 +1639,165 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( assert.strictEqual(igor.messages.at(0)?.body, 'Well I thought the title was fine'); }); + deprecatedTest( + 'Type can be inferred from the key of a hasMany relationship', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 1 }, + async function (assert) { + assert.expect(1); + + const User = Model.extend({ + name: attr(), + contacts: hasMany({ inverse: null, async: false }), + }); + + const Contact = Model.extend({ + name: attr(), + user: belongsTo('user', { async: false, inverse: null }), + }); + + this.owner.register('model:user', User); + this.owner.register('model:contact', Contact); + + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + adapter.findRecord = function () { + return { + data: { + id: '1', + type: 'user', + relationships: { + contacts: { + data: [{ id: '1', type: 'contact' }], + }, + }, + }, + }; + }; + + const user = store.push({ + data: { + type: 'user', + id: '1', + relationships: { + contacts: { + data: [{ type: 'contact', id: '1' }], + }, + }, + }, + included: [ + { + type: 'contact', + id: '1', + }, + ], + }); + const contacts = await user.contacts; + assert.strictEqual(contacts.length, 1, 'The contacts relationship is correctly set up'); + } + ); + + deprecatedTest( + 'Type can be inferred from the key of an async hasMany relationship', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 1 }, + async function (assert) { + class User extends Model { + @attr name; + @hasMany('message', { polymorphic: true, async: false, inverse: 'user' }) messages; + @hasMany({ async: true, inverse: null }) contacts; + } + this.owner.register('model:user', User); + + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + adapter.findRecord = function (store, type, ids, snapshots) { + return { + data: { + id: '1', + type: 'user', + relationships: { + contacts: { + data: [{ id: '1', type: 'contact' }], + }, + }, + }, + }; + }; + + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + contacts: { + data: [{ type: 'contact', id: '1' }], + }, + }, + }, + included: [ + { + type: 'contact', + id: '1', + }, + ], + }); + + const user = await store.findRecord('user', '1'); + const contacts = await user.contacts; + assert.strictEqual(contacts.length, 1, 'The contacts relationship is correctly set up'); + } + ); + + deprecatedTest( + 'Polymorphic relationships work with a hasMany whose type is inferred', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 1 }, + async function (assert) { + class User extends Model { + @attr name; + @hasMany('message', { polymorphic: true, async: false, inverse: 'user' }) messages; + @hasMany({ async: false, polymorphic: true, inverse: null }) contacts; + } + this.owner.register('model:user', User); + + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + adapter.findRecord = function (store, type, ids, snapshots) { + return { data: { id: '1', type: 'user' } }; + }; + + store.push({ + data: { + type: 'user', + id: '1', + relationships: { + contacts: { + data: [ + { type: 'email', id: '1' }, + { type: 'phone', id: '2' }, + ], + }, + }, + }, + included: [ + { + type: 'email', + id: '1', + }, + { + type: 'phone', + id: '2', + }, + ], + }); + const user = await store.findRecord('user', '1'); + const contacts = await user.contacts; + + assert.strictEqual(contacts.length, 2, 'The contacts relationship is correctly set up'); + } + ); + test('Polymorphic relationships work with a hasMany whose inverse is null', async function (assert) { assert.expect(1); class User extends Model { @@ -1700,7 +1913,9 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( } catch (e) { assert.strictEqual( e.message, - "The 'post' type does not implement 'comment' and thus cannot be assigned to the 'comments' relationship in 'post'. If this relationship should be polymorphic, mark post.comments as `polymorphic: true` and post.message as implementing it via `as: 'comment'`.", + DEPRECATE_NON_EXPLICIT_POLYMORPHISM + ? "The 'post' type does not implement 'comment' and thus cannot be assigned to the 'comments' relationship in 'post'. Make it a descendant of 'comment' or use a mixin of the same name." + : "The 'post' type does not implement 'comment' and thus cannot be assigned to the 'comments' relationship in 'post'. If this relationship should be polymorphic, mark post.comments as `polymorphic: true` and post.message as implementing it via `as: 'comment'`.", 'should throw' ); } @@ -1775,7 +1990,9 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( function () { user.messages.push(anotherUser); }, - `The schema for the relationship 'user' on 'user' type does not correctly implement 'message' and thus cannot be assigned to the 'messages' relationship in 'user'. If using this record in this polymorphic relationship is desired, correct the errors in the schema shown below: + DEPRECATE_NON_EXPLICIT_POLYMORPHISM + ? "The 'user' type does not implement 'message' and thus cannot be assigned to the 'messages' relationship in 'user'. Make it a descendant of 'message' or use a mixin of the same name." + : `The schema for the relationship 'user' on 'user' type does not correctly implement 'message' and thus cannot be assigned to the 'messages' relationship in 'user'. If using this record in this polymorphic relationship is desired, correct the errors in the schema shown below: \`\`\` { @@ -1907,7 +2124,9 @@ module('integration/relationships/has_many - Has-Many Relationships', function ( function () { user.messages.push(anotherUser); }, - `No 'user' field exists on 'user'. To use this type in the polymorphic relationship 'user.messages' the relationships schema definition for user should include: + DEPRECATE_NON_EXPLICIT_POLYMORPHISM + ? "The 'user' type does not implement 'message' and thus cannot be assigned to the 'messages' relationship in 'user'. Make it a descendant of 'message' or use a mixin of the same name." + : `No 'user' field exists on 'user'. To use this type in the polymorphic relationship 'user.messages' the relationships schema definition for user should include: \`\`\` { @@ -2753,6 +2972,21 @@ If using this relationship in a polymorphic manner is desired, the relationships assert.strictEqual(page.chapter, chapter, 'Page has a chapter after rollback attributes'); }); + testInDebug('Passing a model as type to hasMany should not work', function (assert) { + assert.expect(3); + + assert.expectAssertion(() => { + const User = Model.extend(); + + Model.extend({ + users: hasMany(User, { async: false, inverse: null }), + }); + }, /The first argument to hasMany must be a string/); + + assert.expectDeprecation({ id: 'ember-data:deprecate-early-static' }); + assert.expectDeprecation({ id: 'ember-data:deprecate-non-strict-relationships' }); + }); + test('Relationship.clear removes all records correctly', async function (assert) { class Post extends Model { @attr title; @@ -2822,7 +3056,13 @@ If using this relationship in a polymorphic manner is desired, the relationships ); const postComments = await post.comments; - postComments.length = 0; + + if (DEPRECATE_ARRAY_LIKE) { + postComments.clear(); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + } else { + postComments.length = 0; + } assert.deepEqual( comments.map((comment) => comment.post), @@ -3656,6 +3896,54 @@ If using this relationship in a polymorphic manner is desired, the relationships }); }); + deprecatedTest( + 'PromiseArray proxies createRecord to its ManyArray before the hasMany is loaded', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 1 }, + async function (assert) { + class Post extends Model { + @attr title; + @hasMany('comment', { async: true, inverse: 'message' }) comments; + } + class Comment extends Model { + @attr body; + @belongsTo('post', { async: false, inverse: 'comments' }) message; + } + this.owner.register('model:post', Post); + this.owner.register('model:comment', Comment); + + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + adapter.findHasMany = function (store, record, link, relationship) { + return Promise.resolve({ + data: [ + { id: '1', type: 'comment', attributes: { body: 'First' } }, + { id: '2', type: 'comment', attributes: { body: 'Second' } }, + ], + }); + }; + + const post = store.push({ + data: { + type: 'post', + id: '1', + relationships: { + comments: { + links: { + related: 'someLink', + }, + }, + }, + }, + }); + + const commentsPromise = post.comments; + commentsPromise.createRecord(); + const comments = await commentsPromise; + assert.strictEqual(comments.length, 3, 'comments have 3 length, including new record'); + } + ); + test('deleteRecord + unloadRecord', async function (assert) { class User extends Model { @attr name; @@ -4029,4 +4317,181 @@ If using this relationship in a polymorphic manner is desired, the relationships assert.strictEqual(person.phoneNumbers.length, 1); } ); + + deprecatedTest( + 'a synchronous hasMany record array should only remove object(s) if found in collection', + { + id: 'ember-data:deprecate-array-like', + count: 3, + until: '5.0', + }, + async function (assert) { + class Person extends Model { + @attr() + name; + @belongsTo('tag', { async: false, inverse: 'people' }) + tag; + } + + class Tag extends Model { + @hasMany('person', { async: false, inverse: 'tag' }) + people; + } + + this.owner.register('model:person', Person); + this.owner.register('model:tag', Tag); + + const store = this.owner.lookup('service:store'); + // eslint-disable-next-line no-unused-vars + const [tag, scumbagInRecordArray, _person2, scumbagNotInRecordArray] = store.push({ + data: [ + { + type: 'tag', + id: '1', + relationships: { + people: { + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + ], + }, + }, + }, + { + type: 'person', + id: '1', + attributes: { + name: 'Scumbag Dale', + }, + }, + { + type: 'person', + id: '2', + attributes: { + name: 'Scumbag Tom', + }, + }, + { + type: 'person', + id: '3', + attributes: { + name: 'Scumbag Ross', + }, + }, + ], + }); + + const recordArray = tag.people; + + recordArray.removeObject(scumbagNotInRecordArray); + + assert.strictEqual( + recordArray.length, + 2, + 'Record array unchanged after attempting to remove object not found in collection' + ); + + recordArray.removeObject(scumbagInRecordArray); + + let didRemoveObject = recordArray.length === 1 && !recordArray.includes(scumbagInRecordArray); + assert.true(didRemoveObject, 'Record array successfully removed expected object from collection'); + + recordArray.push(scumbagInRecordArray); + + const scumbagsToRemove = [scumbagInRecordArray, scumbagNotInRecordArray]; + recordArray.removeObjects(scumbagsToRemove); + + didRemoveObject = recordArray.length === 1 && !recordArray.includes(scumbagInRecordArray); + assert.true(didRemoveObject, 'Record array only removes objects in list that are found in collection'); + } + ); + + deprecatedTest( + 'an asynchronous hasMany record array should only remove object(s) if found in collection', + { + id: 'ember-data:deprecate-promise-many-array-behaviors', + count: 6, + until: '5.0', + }, + async function (assert) { + class Person extends Model { + @attr() + name; + @belongsTo('tag', { async: false, inverse: 'people' }) + tag; + } + + class Tag extends Model { + @hasMany('person', { async: true, inverse: 'tag' }) + people; + } + + const store = this.owner.lookup('service:store'); + this.owner.register('model:person', Person); + this.owner.register('model:tag', Tag); + + // eslint-disable-next-line no-unused-vars + const [tag, scumbagInRecordArray, _person2, scumbagNotInRecordArray] = store.push({ + data: [ + { + type: 'tag', + id: '1', + relationships: { + people: { + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + ], + }, + }, + }, + { + type: 'person', + id: '1', + attributes: { + name: 'Scumbag Dale', + }, + }, + { + type: 'person', + id: '2', + attributes: { + name: 'Scumbag Tom', + }, + }, + { + type: 'person', + id: '3', + attributes: { + name: 'Scumbag Ross', + }, + }, + ], + }); + + const recordArray = tag.people; + + recordArray.removeObject(scumbagNotInRecordArray); + + assert.strictEqual( + recordArray.length, + 2, + 'Record array unchanged after attempting to remove object not found in collection' + ); + + recordArray.removeObject(scumbagInRecordArray); + + let didRemoveObject = recordArray.length === 1 && !recordArray.includes(scumbagInRecordArray); + assert.true(didRemoveObject, 'Record array successfully removed expected object from collection'); + + recordArray.pushObject(scumbagInRecordArray); + + const scumbagsToRemove = [scumbagInRecordArray, scumbagNotInRecordArray]; + recordArray.removeObjects(scumbagsToRemove); + + didRemoveObject = recordArray.length === 1 && !recordArray.includes(scumbagInRecordArray); + assert.true(didRemoveObject, 'Record array only removes objects in list that are found in collection'); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like', count: 4 }); + } + ); }); diff --git a/tests/main/tests/integration/relationships/inverse-relationship-load-test.js b/tests/main/tests/integration/relationships/inverse-relationship-load-test.js index c9b518518dd..3641620be67 100644 --- a/tests/main/tests/integration/relationships/inverse-relationship-load-test.js +++ b/tests/main/tests/integration/relationships/inverse-relationship-load-test.js @@ -5,6 +5,7 @@ import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; module('inverse relationship load test', function (hooks) { let store; @@ -23,6 +24,186 @@ module('inverse relationship load test', function (hooks) { ); }); + deprecatedTest( + 'one-to-many - findHasMany/implicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); + const dog1 = dogs.at(0); + const dogPerson1 = await dog1.person; + assert.strictEqual( + dogPerson1.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + const dogPerson2 = await dogs.at(1).person; + assert.strictEqual( + dogPerson2.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + + await dog1.destroyRecord(); + assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); + } + ); + + deprecatedTest( + 'one-to-many (left hand async, right hand sync) - findHasMany/implicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: false, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); + const dog1 = dogs.at(0); + const dogPerson1 = await dog1.person; + assert.strictEqual( + dogPerson1.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + const dogPerson2 = await dogs.at(1).person; + assert.strictEqual( + dogPerson2.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + + await dog1.destroyRecord(); + assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); + } + ); + test('one-to-many - findHasMany/explicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', async function (assert) { const { owner } = this; @@ -282,6 +463,158 @@ module('inverse relationship load test', function (hooks) { assert.strictEqual(dogs.at(0).id, '2'); }); + deprecatedTest( + 'one-to-one - findBelongsTo/implicit inverse - ensures inverse relationship is set up when payload does not return parent relationship info', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord() { + return Promise.resolve({ + data: null, + }); + }, + findBelongsTo() { + return Promise.resolve({ + data: { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @attr() + name; + @belongsTo('dog', { async: true }) + favoriteDog; + } + owner.register('model:person', Person); + + class Dog extends Model { + @attr() + name; + @belongsTo('person', { async: true }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + favoriteDog: { + links: { + related: 'http://example.com/person/1/favorite-dog', + }, + }, + }, + }, + }); + + let favoriteDog = await person.favoriteDog; + assert.false(person.belongsTo('favoriteDog').belongsToRelationship.state.isEmpty); + assert.strictEqual(favoriteDog.id, '1', 'favoriteDog id is set correctly'); + const favoriteDogPerson = await favoriteDog.person; + assert.strictEqual( + favoriteDogPerson.id, + '1', + 'favoriteDog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + await favoriteDog.destroyRecord(); + favoriteDog = await person.favoriteDog; + assert.strictEqual(favoriteDog, null); + } + ); + + deprecatedTest( + 'one-to-one (left hand async, right hand sync) - findBelongsTo/implicit inverse - ensures inverse relationship is set up when payload does not return parent relationship info', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord() { + return Promise.resolve({ + data: null, + }); + }, + findBelongsTo() { + return Promise.resolve({ + data: { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @attr() + name; + @belongsTo('dog', { async: true }) + favoriteDog; + } + owner.register('model:person', Person); + + class Dog extends Model { + @attr() + name; + @belongsTo('person', { async: true }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + favoriteDog: { + links: { + related: 'http://example.com/person/1/favorite-dog', + }, + }, + }, + }, + }); + + let favoriteDog = await person.favoriteDog; + assert.false(person.belongsTo('favoriteDog').belongsToRelationship.state.isEmpty); + assert.strictEqual(favoriteDog.id, '1', 'favoriteDog id is set correctly'); + const favoriteDogPerson = await favoriteDog.person; + assert.strictEqual( + favoriteDogPerson.id, + '1', + 'favoriteDog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + await favoriteDog.destroyRecord(); + favoriteDog = await person.favoriteDog; + assert.strictEqual(favoriteDog, null); + } + ); + test('one-to-one - findBelongsTo/explicit inverse - ensures inverse relationship is set up when payload does not return parent relationship info', async function (assert) { const { owner } = this; @@ -490,6 +823,176 @@ module('inverse relationship load test', function (hooks) { assert.strictEqual(favoriteDog, null); }); + deprecatedTest( + 'many-to-many - findHasMany/implicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @hasMany('person', { + async: true, + }) + walkers; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty); + + assert.strictEqual(dogs.length, 2, 'left hand side relationship is set up with correct number of records'); + const [dog1, dog2] = dogs.slice(); + const dog1Walkers = await dog1.walkers; + assert.strictEqual(dog1Walkers.length, 1, 'dog1.walkers inverse relationship includes correct number of records'); + assert.strictEqual(dog1Walkers.at(0).id, '1', 'dog1.walkers inverse relationship is set up correctly'); + + const dog2Walkers = await dog2.walkers; + assert.strictEqual(dog2Walkers.length, 1, 'dog2.walkers inverse relationship includes correct number of records'); + assert.strictEqual(dog2Walkers.at(0).id, '1', 'dog2.walkers inverse relationship is set up correctly'); + + await dog1.destroyRecord(); + assert.strictEqual(dogs.length, 1, 'person.dogs relationship was updated when record removed'); + assert.strictEqual(dogs.at(0).id, '2', 'person.dogs relationship has the correct records'); + } + ); + + deprecatedTest( + 'many-to-many (left hand async, right hand sync) - findHasMany/implicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @hasMany('person', { + async: false, + }) + walkers; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty); + + assert.strictEqual(dogs.length, 2, 'left hand side relationship is set up with correct number of records'); + const [dog1, dog2] = dogs.slice(); + const dog1Walkers = await dog1.walkers; + assert.strictEqual(dog1Walkers.length, 1, 'dog1.walkers inverse relationship includes correct number of records'); + assert.strictEqual(dog1Walkers.at(0).id, '1', 'dog1.walkers inverse relationship is set up correctly'); + + const dog2Walkers = await dog2.walkers; + assert.strictEqual(dog2Walkers.length, 1, 'dog2.walkers inverse relationship includes correct number of records'); + assert.strictEqual(dog2Walkers.at(0).id, '1', 'dog2.walkers inverse relationship is set up correctly'); + + await dog1.destroyRecord(); + assert.strictEqual(dogs.length, 1, 'person.dogs relationship was updated when record removed'); + assert.strictEqual(dogs.at(0).id, '2', 'person.dogs relationship has the correct records'); + } + ); + test('many-to-many - findHasMany/explicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', async function (assert) { const { owner } = this; @@ -656,6 +1159,158 @@ module('inverse relationship load test', function (hooks) { assert.strictEqual(dogs.at(0).id, '2', 'person.dogs relationship has the correct records'); }); + deprecatedTest( + 'many-to-one - findBelongsTo/implicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + let dog = store.push({ + data: { + type: 'dog', + id: '1', + attributes: { + name: 'A Really Good Dog', + }, + relationships: { + person: { + links: { + related: 'http://example.com/person/1', + }, + }, + }, + }, + }); + + const person = await dog.person; + assert.false( + dog.belongsTo('person').belongsToRelationship.state.isEmpty, + 'belongsTo relationship state was populated' + ); + assert.strictEqual(person.id, '1', 'dog.person relationship is correctly set up'); + + const dogs = await person.dogs; + + assert.strictEqual(dogs.length, 1, 'person.dogs inverse relationship includes correct number of records'); + const [dog1] = dogs.slice(); + assert.strictEqual(dog1.id, '1', 'dog1.person inverse relationship is set up correctly'); + + await person.destroyRecord(); + dog = await dog.person; + assert.strictEqual(dog, null, 'record deleted removed from belongsTo relationship'); + } + ); + + deprecatedTest( + 'many-to-one (left hand async, right hand sync) - findBelongsTo/implicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: false, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + let dog = store.push({ + data: { + type: 'dog', + id: '1', + attributes: { + name: 'A Really Good Dog', + }, + relationships: { + person: { + links: { + related: 'http://example.com/person/1', + }, + }, + }, + }, + }); + + const person = await dog.person; + assert.false( + dog.belongsTo('person').belongsToRelationship.state.isEmpty, + 'belongsTo relationship state was populated' + ); + assert.strictEqual(person.id, '1', 'dog.person relationship is correctly set up'); + + const dogs = await person.dogs; + + assert.strictEqual(dogs.length, 1, 'person.dogs inverse relationship includes correct number of records'); + const [dog1] = dogs.slice(); + assert.strictEqual(dog1.id, '1', 'dog1.person inverse relationship is set up correctly'); + + await person.destroyRecord(); + dog = await dog.person; + assert.strictEqual(dog, null, 'record deleted removed from belongsTo relationship'); + } + ); + test('many-to-one - findBelongsTo/explicit inverse - adds parent relationship information to the payload if it is not included/added by the serializer', async function (assert) { const { owner } = this; @@ -804,91 +1459,1701 @@ module('inverse relationship load test', function (hooks) { assert.strictEqual(dog, null, 'record deleted removed from belongsTo relationship'); }); - test('one-to-many - ids/non-link/explicit inverse - ids - records loaded through ids/findRecord are linked to the parent if the response from the server does not include relationship information', async function (assert) { - const { owner } = this; - - const scooby = { - id: '1', - type: 'dog', - attributes: { - name: 'Scooby', - }, - }; - - const scrappy = { - id: '2', - type: 'dog', - attributes: { - name: 'Scrappy', - }, - }; - - owner.register( - 'adapter:application', - JSONAPIAdapter.extend({ - deleteRecord: () => Promise.resolve({ data: null }), - findRecord: (_store, _type, id) => { - const dog = id === '1' ? scooby : scrappy; - return Promise.resolve({ - data: dog, - }); + deprecatedTest( + 'one-to-many - findHasMany/implicit inverse - fixes mismatched parent relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, }, - }) - ); + }); - class Person extends Model { - @hasMany('dog', { - async: true, - inverse: 'pal', - }) - dogs; + await assert.expectAssertion(async () => { + await person.dogs; + }, /The record loaded at/); } - owner.register('model:person', Person); + ); + + deprecatedTest( + 'one-to-many (left hand async, right hand sync) - findHasMany/implicit inverse - fixes mismatched parent relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: false, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); - class Dog extends Model { - @belongsTo('person', { - async: true, - inverse: 'dogs', - }) - pal; + await assert.expectAssertion(async () => { + await person.dogs; + }, /The record loaded at/); } - owner.register('model:dog', Dog); + ); + + deprecatedTest( + 'one-to-many - findHasMany/implicit inverse - fixes null relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: null, + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + person: { + data: null, + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); - const person = store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'John Churchill', + await assert.expectAssertion(async () => { + await person.dogs; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'one-to-many (left hand async, right hand sync) - findHasMany/implicit inverse - fixes null relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: null, + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + person: { + data: null, + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: false, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, }, - relationships: { - dogs: { - data: [ - { + }); + + await assert.expectAssertion(async () => { + await person.dogs; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'one-to-one - findBelongsTo/implicit inverse - fixes mismatched parent relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { id: '1', type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, }, - { - id: '2', - type: 'dog', + }); + }, + }) + ); + + class Person extends Model { + @belongsTo('dog', { + async: true, + }) + dog; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dog: { + links: { + related: 'http://example.com/person/1/dog', }, - ], + }, }, }, - }, - }); + }); - const dogs = await person.dogs; - assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + await assert.expectAssertion(async () => { + await person.dog; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'one-to-one (left hand async, right hand sync) - findBelongsTo/implicit inverse - fixes mismatched parent relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @belongsTo('dog', { + async: true, + }) + dog; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: false, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dog: { + links: { + related: 'http://example.com/person/1/dog', + }, + }, + }, + }, + }); - assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); - const dog1 = dogs.at(0); - const dogPerson1 = await dog1.pal; - assert.strictEqual( - dogPerson1.id, - '1', - 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' - ); + await assert.expectAssertion(async () => { + await person.dog; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'one-to-one - findBelongsTo/implicit inverse - fixes null relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: null, + }, + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @belongsTo('dog', { + async: true, + }) + dog; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dog: { + links: { + related: 'http://example.com/person/1/dog', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await person.dog; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'one-to-one (left hand async, right hand sync) - findBelongsTo/implicit inverse - fixes null relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: null, + }, + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @belongsTo('dog', { + async: true, + }) + dog; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: false, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dog: { + links: { + related: 'http://example.com/person/1/dog', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await person.dog; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-one - findBelongsTo/implicitInverse - fixes mismatched parent relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const dog = store.push({ + data: { + type: 'dog', + id: '1', + attributes: { + name: 'A Really Good Dog', + }, + relationships: { + person: { + links: { + related: 'http://example.com/person/1', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await dog.person; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-one (left hand async, right hand sync) - findBelongsTo/implicitInverse - fixes mismatched parent relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: false, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const dog = store.push({ + data: { + type: 'dog', + id: '1', + attributes: { + name: 'A Really Good Dog', + }, + relationships: { + person: { + links: { + related: 'http://example.com/person/1', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await dog.person; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-one - findBelongsTo/implicitInverse - fixes null relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [], + }, + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const dog = store.push({ + data: { + type: 'dog', + id: '1', + attributes: { + name: 'A Really Good Dog', + }, + relationships: { + person: { + links: { + related: 'http://example.com/person/1', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await dog.person; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-one (left hand async, right hand sync) - findBelongsTo/implicitInverse - fixes null relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findBelongsTo: () => { + return Promise.resolve({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [], + }, + }, + }, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: false, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const dog = store.push({ + data: { + type: 'dog', + id: '1', + attributes: { + name: 'A Really Good Dog', + }, + relationships: { + person: { + links: { + related: 'http://example.com/person/1', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await dog.person; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-many - findHasMany/implicitInverse - fixes mismatched parent relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + walkers: { + data: [ + { + id: '2', + type: 'person', + }, + ], + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + walkers: { + data: [ + { + id: '2', + type: 'person', + }, + ], + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @hasMany('person', { + async: true, + }) + walkers; + } + owner.register('model:dog', Dog); + + const person1 = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await person1.dogs; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-many (left hand async, right hand sync) - findHasMany/implicitInverse - fixes mismatched parent relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + walkers: { + data: [ + { + id: '2', + type: 'person', + }, + ], + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + walkers: { + data: [ + { + id: '2', + type: 'person', + }, + ], + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @hasMany('person', { + async: false, + }) + walkers; + } + owner.register('model:dog', Dog); + + const person1 = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await person1.dogs; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-many - findHasMany/implicitInverse - fixes empty relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + walkers: { + data: [], + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + walkers: { + data: [], + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @hasMany('person', { + async: true, + }) + walkers; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await person.dogs; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-many (left hand async, right hand sync) - findHasMany/implicitInverse - fixes empty relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + walkers: { + data: [], + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + walkers: { + data: [], + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @hasMany('person', { + async: false, + }) + walkers; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await person.dogs; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-many - findHasMany/implicitInverse - fixes null relationship information from the payload and deprecates', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + walkers: { + data: null, + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + walkers: { + data: null, + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @hasMany('person', { + async: true, + }) + walkers; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await person.dogs; + }, /The record loaded at/); + } + ); + + deprecatedTest( + 'many-to-many (left hand async, right hand sync) - findHasMany/implicitInverse - asserts incorrect null relationship information from the payload', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findHasMany: () => { + return Promise.resolve({ + data: [ + { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + walkers: { + data: null, + }, + }, + }, + { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + walkers: { + data: null, + }, + }, + }, + ], + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @hasMany('person', { + async: false, + }) + walkers; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + links: { + related: 'http://example.com/person/1/dogs', + }, + }, + }, + }, + }); + + await assert.expectAssertion(async () => { + await person.dogs; + }, /The record loaded at data\[0\] in the payload specified null as its/); + } + ); + + deprecatedTest( + 'one-to-many - ids/non-link/implicit inverse - ids - records loaded through ids/findRecord are linked to the parent if the response from the server does not include relationship information', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + const scooby = { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }; + + const scrappy = { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + }; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findRecord: (_store, _type, id) => { + const dog = id === '1' ? scooby : scrappy; + return Promise.resolve({ + data: dog, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '1', + type: 'dog', + }, + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); + const dog1 = dogs.at(0); + const dogPerson1 = await dog1.person; + assert.strictEqual( + dogPerson1.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + const dogPerson2 = await dogs.at(1).person; + assert.strictEqual( + dogPerson2.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + + await dog1.destroyRecord(); + assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); + } + ); + + deprecatedTest( + 'one-to-many (left hand async, right hand sync) - ids/non-link/implicit inverse - ids - records loaded through ids/findRecord are linked to the parent if the response from the server does not include relationship information', + { id: 'ember-data:deprecate-non-strict-relationships', debugOnly: true, until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + const scooby = { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }; + + const scrappy = { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + }; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findRecord: (_store, _type, id) => { + const dog = id === '1' ? scooby : scrappy; + return Promise.resolve({ + data: dog, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: false, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '1', + type: 'dog', + }, + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); + const dog1 = dogs.at(0); + const dogPerson1 = await dog1.person; + assert.strictEqual( + dogPerson1.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + const dogPerson2 = await dogs.at(1).person; + assert.strictEqual( + dogPerson2.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); + + await dog1.destroyRecord(); + assert.strictEqual(dogs.length, 1, 'record removed from hasMany relationship after deletion'); + assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); + } + ); + + test('one-to-many - ids/non-link/explicit inverse - ids - records loaded through ids/findRecord are linked to the parent if the response from the server does not include relationship information', async function (assert) { + const { owner } = this; + + const scooby = { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + }; + + const scrappy = { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + }; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findRecord: (_store, _type, id) => { + const dog = id === '1' ? scooby : scrappy; + return Promise.resolve({ + data: dog, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + inverse: 'pal', + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + inverse: 'dogs', + }) + pal; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '1', + type: 'dog', + }, + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(dogs.length, 2, 'hasMany relationship has correct number of records'); + const dog1 = dogs.at(0); + const dogPerson1 = await dog1.pal; + assert.strictEqual( + dogPerson1.id, + '1', + 'dog.person inverse relationship is set up correctly when adapter does not include parent relationships in data.relationships' + ); const dogPerson2 = await dogs.at(1).pal; assert.strictEqual( dogPerson2.id, @@ -1077,6 +3342,468 @@ module('inverse relationship load test', function (hooks) { assert.strictEqual(dogs.at(0).id, '2', 'hasMany relationship has correct records'); }); + deprecatedTest( + 'one-to-many - ids/non-link/implicit inverse - records loaded through ids/findRecord do not get associated with the parent if the server specifies another resource as the relationship value in the response', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + const scooby = { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }; + + const scrappy = { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findRecord: (_store, _type, id) => { + const dog = id === '1' ? scooby : scrappy; + return Promise.resolve({ + data: dog, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '1', + type: 'dog', + }, + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + + const person2 = store.push({ + data: { + type: 'person', + id: '2', + attributes: { + name: 'ok', + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(dogs.length, 0, 'hasMany relationship for parent is empty'); + + const person2Dogs = await person2.dogs; + assert.strictEqual( + person2Dogs.length, + 2, + 'hasMany relationship on specified record has correct number of associated records' + ); + + const allDogs = store.peekAll('dogs').slice(); + for (let i = 0; i < allDogs.length; i++) { + const dog = allDogs[i]; + const dogPerson = await dog.person; + assert.strictEqual(dogPerson.id, person2.id, 'right hand side has correct belongsTo value'); + } + + const dog1 = store.peekRecord('dog', '1'); + await dog1.destroyRecord(); + assert.strictEqual(person2Dogs.length, 1, 'record removed from hasMany relationship after deletion'); + assert.strictEqual(person2Dogs.at(0).id, '2', 'hasMany relationship has correct records'); + } + ); + + deprecatedTest( + 'one-to-many (left hand async, right hand sync) - ids/non-link/implicit inverse - records loaded through ids/findRecord do not get associated with the parent if the server specifies another resource as the relationship value in the response', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + const scooby = { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }; + + const scrappy = { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + person: { + data: { + id: '2', + type: 'person', + }, + }, + }, + }; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findRecord: (_store, _type, id) => { + const dog = id === '1' ? scooby : scrappy; + return Promise.resolve({ + data: dog, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: false, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '1', + type: 'dog', + }, + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + + const person2 = store.push({ + data: { + type: 'person', + id: '2', + attributes: { + name: 'ok', + }, + }, + }); + + const dogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(dogs.length, 0, 'hasMany relationship for parent is empty'); + + const person2Dogs = await person2.dogs; + assert.strictEqual( + person2Dogs.length, + 2, + 'hasMany relationship on specified record has correct number of associated records' + ); + + const allDogs = store.peekAll('dogs').slice(); + for (let i = 0; i < allDogs.length; i++) { + const dog = allDogs[i]; + const dogPerson = await dog.person; + assert.strictEqual(dogPerson.id, person2.id, 'right hand side has correct belongsTo value'); + } + + const dog1 = store.peekRecord('dog', '1'); + await dog1.destroyRecord(); + assert.strictEqual(person2Dogs.length, 1, 'record removed from hasMany relationship after deletion'); + assert.strictEqual(person2Dogs.at(0).id, '2', 'hasMany relationship has correct records'); + } + ); + + deprecatedTest( + 'one-to-many - ids/non-link/implicit inverse - records loaded through ids/findRecord do not get associated with the parent if the server specifies null as the relationship value in the response', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + const scooby = { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: null, + }, + }, + }; + + const scrappy = { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + person: { + data: null, + }, + }, + }; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findRecord: (_store, _type, id) => { + const dog = id === '1' ? scooby : scrappy; + return Promise.resolve({ + data: dog, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: true, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '1', + type: 'dog', + }, + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + + const personDogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(personDogs.length, 0, 'hasMany relationship for parent is empty'); + + const allDogs = store.peekAll('dogs').slice(); + for (let i = 0; i < allDogs.length; i++) { + const dog = allDogs[i]; + const dogPerson = await dog.person; + assert.strictEqual(dogPerson, null, 'right hand side has correct belongsTo value'); + } + + const dog1 = store.peekRecord('dog', '1'); + await dog1.destroyRecord(); + + assert.strictEqual(personDogs.length, 0); + } + ); + + deprecatedTest( + 'one-to-many (left hand async, right hand sync) - ids/non-link/implicit inverse - records loaded through ids/findRecord do not get associated with the parent if the server specifies null as the relationship value in the response', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 2 }, + async function (assert) { + const { owner } = this; + + const scooby = { + id: '1', + type: 'dog', + attributes: { + name: 'Scooby', + }, + relationships: { + person: { + data: null, + }, + }, + }; + + const scrappy = { + id: '2', + type: 'dog', + attributes: { + name: 'Scrappy', + }, + relationships: { + person: { + data: null, + }, + }, + }; + + owner.register( + 'adapter:application', + JSONAPIAdapter.extend({ + deleteRecord: () => Promise.resolve({ data: null }), + findRecord: (_store, _type, id) => { + const dog = id === '1' ? scooby : scrappy; + return Promise.resolve({ + data: dog, + }); + }, + }) + ); + + class Person extends Model { + @hasMany('dog', { + async: true, + }) + dogs; + } + owner.register('model:person', Person); + + class Dog extends Model { + @belongsTo('person', { + async: false, + }) + person; + } + owner.register('model:dog', Dog); + + const person = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'John Churchill', + }, + relationships: { + dogs: { + data: [ + { + id: '1', + type: 'dog', + }, + { + id: '2', + type: 'dog', + }, + ], + }, + }, + }, + }); + + const personDogs = await person.dogs; + assert.false(person.hasMany('dogs').hasManyRelationship.state.isEmpty, 'relationship state was set up correctly'); + + assert.strictEqual(personDogs.length, 0, 'hasMany relationship for parent is empty'); + + const allDogs = store.peekAll('dogs').slice(); + for (let i = 0; i < allDogs.length; i++) { + const dog = allDogs[i]; + const dogPerson = await dog.person; + assert.strictEqual(dogPerson, null, 'right hand side has correct belongsTo value'); + } + + const dog1 = store.peekRecord('dog', '1'); + await dog1.destroyRecord(); + + assert.strictEqual(personDogs.length, 0); + } + ); + test('one-to-many - ids/non-link/explicit inverse - records loaded through ids/findRecord do not get associated with the parent if the server specifies another resource as the relationship value in the response', async function (assert) { const { owner } = this; diff --git a/tests/main/tests/integration/relationships/inverse-relationships-test.js b/tests/main/tests/integration/relationships/inverse-relationships-test.js index 6ad758ca038..b2e143bc78d 100644 --- a/tests/main/tests/integration/relationships/inverse-relationships-test.js +++ b/tests/main/tests/integration/relationships/inverse-relationships-test.js @@ -2,8 +2,16 @@ import { module } from 'qunit'; import { setupTest } from 'ember-qunit'; -import Model, { belongsTo, hasMany } from '@ember-data/model'; +import { graphFor } from '@ember-data/graph/-private'; +import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { recordIdentifierFor } from '@ember-data/store'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE } from '@warp-drive/build-config/deprecations'; + +function test(label, callback) { + deprecatedTest(label, { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 'ALL' }, callback); +} module('integration/relationships/inverse_relationships - Inverse Relationships', function (hooks) { setupTest(hooks); @@ -18,6 +26,415 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' register = owner.register.bind(owner); }); + test('When a record is added to a has-many relationship, the inverse belongsTo is determined automatically', async function (assert) { + class Post extends Model { + @hasMany('comment', { async: false }) + comments; + } + + class Comment extends Model { + @belongsTo('post', { async: false }) + post; + } + + register('model:Post', Post); + register('model:Comment', Comment); + + const comment = store.createRecord('comment'); + const post = store.createRecord('post'); + + assert.strictEqual(comment.post, null, 'no post has been set on the comment'); + + post.comments.push(comment); + assert.strictEqual(comment.post, post, 'post was set on the comment'); + }); + + test('Inverse relationships can be explicitly nullable', function (assert) { + class User extends Model { + @hasMany('post', { inverse: 'participants', async: false }) + posts; + } + + class Post extends Model { + @belongsTo('user', { inverse: null, async: false }) + lastParticipant; + + @hasMany('user', { inverse: 'posts', async: false }) + participants; + } + + register('model:User', User); + register('model:Post', Post); + + const user = store.createRecord('user'); + const post = store.createRecord('post'); + + assert.strictEqual(user.inverseFor('posts').name, 'participants', 'User.posts inverse is Post.participants'); + assert.strictEqual(post.inverseFor('lastParticipant'), null, 'Post.lastParticipant has no inverse'); + assert.strictEqual(post.inverseFor('participants').name, 'posts', 'Post.participants inverse is User.posts'); + }); + + test('Null inverses are excluded from potential relationship resolutions', function (assert) { + class User extends Model { + @hasMany('post', { async: false }) + posts; + } + + class Post extends Model { + @belongsTo('user', { inverse: null, async: false }) + lastParticipant; + + @hasMany('user', { async: false }) + participants; + } + + register('model:User', User); + register('model:Post', Post); + + const user = store.createRecord('user'); + const post = store.createRecord('post'); + + assert.strictEqual(user.inverseFor('posts').name, 'participants', 'User.posts inverse is Post.participants'); + assert.strictEqual(post.inverseFor('lastParticipant'), null, 'Post.lastParticipant has no inverse'); + assert.strictEqual(post.inverseFor('participants').name, 'posts', 'Post.participants inverse is User.posts'); + }); + + test('When a record is added to a has-many relationship, the inverse belongsTo can be set explicitly', async function (assert) { + class Post extends Model { + @hasMany('comment', { inverse: 'redPost', async: false }) + comments; + } + + class Comment extends Model { + @belongsTo('post', { async: false }) + onePost; + + @belongsTo('post', { async: false }) + twoPost; + + @belongsTo('post', { async: false }) + redPost; + + @belongsTo('post', { async: false }) + bluePost; + } + + register('model:Post', Post); + register('model:Comment', Comment); + + const comment = store.createRecord('comment'); + const post = store.createRecord('post'); + + assert.strictEqual(comment.onePost, null, 'onePost has not been set on the comment'); + assert.strictEqual(comment.twoPost, null, 'twoPost has not been set on the comment'); + assert.strictEqual(comment.redPost, null, 'redPost has not been set on the comment'); + assert.strictEqual(comment.bluePost, null, 'bluePost has not been set on the comment'); + + post.comments.push(comment); + + assert.strictEqual(comment.onePost, null, 'onePost has not been set on the comment'); + assert.strictEqual(comment.twoPost, null, 'twoPost has not been set on the comment'); + assert.strictEqual(comment.redPost, post, 'redPost has been set on the comment'); + assert.strictEqual(comment.bluePost, null, 'bluePost has not been set on the comment'); + }); + + test("When a record's belongsTo relationship is set, it can specify the inverse hasMany to which the new child should be added", async function (assert) { + class Post extends Model { + @hasMany('comment', { async: false }) + meComments; + + @hasMany('comment', { async: false }) + youComments; + + @hasMany('comment', { async: false }) + everyoneWeKnowComments; + } + + class Comment extends Model { + @belongsTo('post', { inverse: 'youComments', async: false }) + post; + } + + register('model:Post', Post); + register('model:Comment', Comment); + + const comment = store.createRecord('comment'); + const post = store.createRecord('post'); + + assert.strictEqual(post.meComments.length, 0, 'meComments has no posts'); + assert.strictEqual(post.youComments.length, 0, 'youComments has no posts'); + assert.strictEqual(post.everyoneWeKnowComments.length, 0, 'everyoneWeKnowComments has no posts'); + + comment.set('post', post); + + assert.strictEqual(comment.post, post, 'The post that was set can be retrieved'); + + assert.strictEqual(post.meComments.length, 0, 'meComments has no posts'); + assert.strictEqual(post.youComments.length, 1, 'youComments had the post added'); + assert.strictEqual(post.everyoneWeKnowComments.length, 0, 'everyoneWeKnowComments has no posts'); + }); + + test('When setting a belongsTo, the OneToOne invariant is respected even when other records have been previously used', async function (assert) { + class Post extends Model { + @belongsTo('comment', { async: false }) + bestComment; + } + + class Comment extends Model { + @belongsTo('post', { async: false }) + post; + } + + register('model:Post', Post); + register('model:Comment', Comment); + + const comment = store.createRecord('comment'); + const post = store.createRecord('post'); + const post2 = store.createRecord('post'); + + comment.set('post', post); + post2.set('bestComment', null); + + assert.strictEqual(comment.post, post); + assert.strictEqual(post.bestComment, comment); + assert.strictEqual(post2.bestComment, null); + + comment.set('post', post2); + + assert.strictEqual(comment.post, post2); + assert.strictEqual(post.bestComment, null); + assert.strictEqual(post2.bestComment, comment); + }); + + test('When setting a belongsTo, the OneToOne invariant is transitive', async function (assert) { + class Post extends Model { + @belongsTo('comment', { async: false }) + bestComment; + } + + class Comment extends Model { + @belongsTo('post', { async: false }) + post; + } + + register('model:Post', Post); + register('model:Comment', Comment); + + const comment = store.createRecord('comment'); + const post = store.createRecord('post'); + const post2 = store.createRecord('post'); + + comment.set('post', post); + + assert.strictEqual(comment.post, post, 'comment post is set correctly'); + assert.strictEqual(post.bestComment, comment, 'post1 comment is set correctly'); + assert.strictEqual(post2.bestComment, null, 'post2 comment is not set'); + + post2.set('bestComment', comment); + + assert.strictEqual(comment.post, post2, 'comment post is set correctly'); + assert.strictEqual(post.bestComment, null, 'post1 comment is no longer set'); + assert.strictEqual(post2.bestComment, comment, 'post2 comment is set correctly'); + }); + + test('When setting a belongsTo, the OneToOne invariant is commutative', async function (assert) { + class Post extends Model { + @belongsTo('comment', { async: false }) + bestComment; + } + + class Comment extends Model { + @belongsTo('post', { async: false }) + post; + } + + register('model:Post', Post); + register('model:Comment', Comment); + + const post = store.createRecord('post'); + const comment = store.createRecord('comment'); + const comment2 = store.createRecord('comment'); + + comment.set('post', post); + + assert.strictEqual(comment.post, post); + assert.strictEqual(post.bestComment, comment); + assert.strictEqual(comment2.post, null); + + post.set('bestComment', comment2); + + assert.strictEqual(comment.post, null); + assert.strictEqual(post.bestComment, comment2); + assert.strictEqual(comment2.post, post); + }); + + test('OneToNone relationship works', async function (assert) { + assert.expect(3); + + class Post extends Model { + @attr('string') + name; + } + + class Comment extends Model { + @belongsTo('post', { async: false }) + post; + } + + register('model:Post', Post); + register('model:Comment', Comment); + + const comment = store.createRecord('comment'); + const post1 = store.createRecord('post'); + const post2 = store.createRecord('post'); + + comment.set('post', post1); + assert.strictEqual(comment.post, post1, 'the post is set to the first one'); + + comment.set('post', post2); + assert.strictEqual(comment.post, post2, 'the post is set to the second one'); + + comment.set('post', post1); + assert.strictEqual(comment.post, post1, 'the post is re-set to the first one'); + }); + + test('When a record is added to or removed from a polymorphic has-many relationship, the inverse belongsTo can be set explicitly', async function (assert) { + class User extends Model { + @hasMany('message', { async: false, inverse: 'redUser', polymorphic: true }) + messages; + } + + class Message extends Model { + @belongsTo('user', { async: false }) + oneUser; + + @belongsTo('user', { async: false }) + twoUser; + + @belongsTo('user', { async: false }) + redUser; + + @belongsTo('user', { async: false }) + blueUser; + } + + class Post extends Message {} + + register('model:User', User); + register('model:Message', Message); + register('model:Post', Post); + + const post = store.createRecord('post'); + const user = store.createRecord('user'); + + assert.strictEqual(post.oneUser, null, 'oneUser has not been set on the user'); + assert.strictEqual(post.twoUser, null, 'twoUser has not been set on the user'); + assert.strictEqual(post.redUser, null, 'redUser has not been set on the user'); + assert.strictEqual(post.blueUser, null, 'blueUser has not been set on the user'); + + user.messages.push(post); + + assert.strictEqual(post.oneUser, null, 'oneUser has not been set on the user'); + assert.strictEqual(post.twoUser, null, 'twoUser has not been set on the user'); + assert.strictEqual(post.redUser, user, 'redUser has been set on the user'); + assert.strictEqual(post.blueUser, null, 'blueUser has not been set on the user'); + + user.messages.pop(); + + assert.strictEqual(post.oneUser, null, 'oneUser has not been set on the user'); + assert.strictEqual(post.twoUser, null, 'twoUser has not been set on the user'); + assert.strictEqual(post.redUser, null, 'redUser has bot been set on the user'); + assert.strictEqual(post.blueUser, null, 'blueUser has not been set on the user'); + + assert.expectDeprecation({ id: 'ember-data:non-explicit-relationships', count: 1 }); + }); + + test("When a record's belongsTo relationship is set, it can specify the inverse polymorphic hasMany to which the new child should be added or removed", async function (assert) { + class User extends Model { + @hasMany('message', { polymorphic: true, async: false }) + meMessages; + + @hasMany('message', { polymorphic: true, async: false }) + youMessages; + + @hasMany('message', { polymorphic: true, async: false }) + everyoneWeKnowMessages; + } + + class Message extends Model { + @belongsTo('user', { inverse: 'youMessages', async: false, as: 'message' }) + user; + } + + class Post extends Message {} + + register('model:User', User); + register('model:Message', Message); + register('model:Post', Post); + + const user = store.createRecord('user'); + const post = store.createRecord('post'); + + assert.strictEqual(user.meMessages.length, 0, 'meMessages has no posts'); + assert.strictEqual(user.youMessages.length, 0, 'youMessages has no posts'); + assert.strictEqual(user.everyoneWeKnowMessages.length, 0, 'everyoneWeKnowMessages has no posts'); + + post.set('user', user); + + assert.strictEqual(user.meMessages.length, 0, 'meMessages has no posts'); + assert.strictEqual(user.youMessages.length, 1, 'youMessages had the post added'); + assert.strictEqual(user.everyoneWeKnowMessages.length, 0, 'everyoneWeKnowMessages has no posts'); + + post.set('user', null); + + assert.strictEqual(user.meMessages.length, 0, 'meMessages has no posts'); + assert.strictEqual(user.youMessages.length, 0, 'youMessages has no posts'); + assert.strictEqual(user.everyoneWeKnowMessages.length, 0, 'everyoneWeKnowMessages has no posts'); + }); + + test("When a record's polymorphic belongsTo relationship is set, it can specify the inverse hasMany to which the new child should be added", async function (assert) { + class Message extends Model { + @hasMany('comment', { inverse: null, async: false }) + meMessages; + + @hasMany('comment', { inverse: 'message', async: false, as: 'message' }) + youMessages; + + @hasMany('comment', { inverse: null, async: false }) + everyoneWeKnowMessages; + } + + class Post extends Message {} + + class Comment extends Message { + @belongsTo('message', { async: false, polymorphic: true, inverse: 'youMessages' }) + message; + } + + register('model:Message', Message); + register('model:Post', Post); + register('model:Comment', Comment); + + const comment = store.createRecord('comment'); + const post = store.createRecord('post'); + + assert.strictEqual(post.meMessages.length, 0, 'meMessages has no posts'); + assert.strictEqual(post.youMessages.length, 0, 'youMessages has no posts'); + assert.strictEqual(post.everyoneWeKnowMessages.length, 0, 'everyoneWeKnowMessages has no posts'); + + comment.set('message', post); + + assert.strictEqual(post.meMessages.length, 0, 'meMessages has no posts'); + assert.strictEqual(post.youMessages.length, 1, 'youMessages had the post added'); + assert.strictEqual(post.everyoneWeKnowMessages.length, 0, 'everyoneWeKnowMessages has no posts'); + + comment.set('message', null); + + assert.strictEqual(post.meMessages.length, 0, 'meMessages has no posts'); + assert.strictEqual(post.youMessages.length, 0, 'youMessages has no posts'); + assert.strictEqual(post.everyoneWeKnowMessages.length, 0, 'everyoneWeKnowMessages has no posts'); + }); + testInDebug("Inverse relationships that don't exist throw a nice error for a hasMany", async function (assert) { class User extends Model {} @@ -36,10 +453,15 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' store.createRecord('comment'); - assert.expectAssertion(function () { - post = store.createRecord('post'); - post.comments; - }, /Expected a relationship schema for 'comment.testPost' to match the inverse of 'post.comments', but no relationship schema was found./); + assert.expectAssertion( + function () { + post = store.createRecord('post'); + post.comments; + }, + DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE + ? /We found no field named 'testPost' on the schema for 'comment' to be the inverse of the 'comments' relationship on 'post'. This is most likely due to a missing field on your model definition./ + : /Expected a relationship schema for 'comment.testPost' to match the inverse of 'post.comments', but no relationship schema was found./ + ); }); testInDebug("Inverse relationships that don't exist throw a nice error for a belongsTo", async function (assert) { @@ -59,10 +481,157 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' let post; store.createRecord('user'); - assert.expectAssertion(function () { - post = store.createRecord('post'); - post.user; - }, /Expected a relationship schema for 'user.testPost' to match the inverse of 'post.user', but no relationship schema was found./); + assert.expectAssertion( + function () { + post = store.createRecord('post'); + post.user; + }, + DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE + ? /We found no field named 'testPost' on the schema for 'user' to be the inverse of the 'user' relationship on 'post'. This is most likely due to a missing field on your model definition./ + : /Expected a relationship schema for 'user.testPost' to match the inverse of 'post.user', but no relationship schema was found./ + ); + }); + + test('inverseFor is only called when inverse is not null', async function (assert) { + assert.expect(2); + + class Post extends Model { + @hasMany('comment', { async: false, inverse: null }) + comments; + } + + class Comment extends Model { + @belongsTo('post', { async: false, inverse: null }) + post; + } + + class User extends Model { + @hasMany('message', { async: false, inverse: 'user' }) + messages; + } + + class Message extends Model { + @belongsTo('user', { async: false, inverse: 'messages' }) + user; + } + + register('model:Post', Post); + register('model:Comment', Comment); + register('model:User', User); + register('model:Message', Message); + + Post.inverseFor = function () { + assert.notOk(true, 'Post model inverseFor is not called'); + }; + + Comment.inverseFor = function () { + assert.notOk(true, 'Comment model inverseFor is not called'); + }; + + Message.inverseFor = function () { + assert.ok(true, 'Message model inverseFor is called'); + }; + + User.inverseFor = function () { + assert.ok(true, 'User model inverseFor is called'); + }; + + store.push({ + data: { + id: '1', + type: 'post', + relationships: { + comments: { + data: [ + { + id: '1', + type: 'comment', + }, + { + id: '2', + type: 'comment', + }, + ], + }, + }, + }, + }); + store.push({ + data: [ + { + id: '1', + type: 'comment', + relationships: { + post: { + data: { + id: '1', + type: 'post', + }, + }, + }, + }, + { + id: '2', + type: 'comment', + relationships: { + post: { + data: { + id: '1', + type: 'post', + }, + }, + }, + }, + ], + }); + store.push({ + data: { + id: '1', + type: 'user', + relationships: { + messages: { + data: [ + { + id: '1', + type: 'message', + }, + { + id: '2', + type: 'message', + }, + ], + }, + }, + }, + }); + store.push({ + data: [ + { + id: '1', + type: 'message', + relationships: { + user: { + data: { + id: '1', + type: 'user', + }, + }, + }, + }, + { + id: '2', + type: 'message', + relationships: { + post: { + data: { + id: '1', + type: 'user', + }, + }, + }, + }, + ], + }); }); testInDebug( @@ -74,25 +643,65 @@ module('integration/relationships/inverse_relationships - Inverse Relationships' } register('model:user', User); - assert.expectAssertion(() => { - store.push({ - data: { - id: '1', - type: 'user', - relationships: { - post: { - data: { - id: '1', - type: 'post', - }, - }, - }, - }, - }); - }, /Missing Schema: Encountered a relationship identifier { type: 'post', id: '1' } for the 'user.post' belongsTo relationship on , but no schema exists for that type./); + assert.expectAssertion( + () => { + store.createRecord('user', { post: null }); + }, + DEPRECATE_RELATIONSHIPS_WITHOUT_INVERSE + ? /No model was found for 'post' and no schema handles the type/ + : /Missing Schema: Encountered a relationship identifier { type: 'post', id: '1' } for the 'user.post' belongsTo relationship on , but no schema exists for that type./ + ); // but don't error if the relationship is not used store.createRecord('user', {}); } ); + + test('No inverse configuration - should default to a null inverse', async function (assert) { + class User extends Model {} + + class Comment extends Model { + @belongsTo('user', { async: true }) + user; + } + + register('model:User', User); + register('model:Comment', Comment); + + const comment = store.createRecord('comment'); + + assert.strictEqual(comment.inverseFor('user'), null, 'Defaults to a null inverse'); + }); + + test('Unload a destroyed record should clean the relations', async function (assert) { + assert.expect(2); + + class Post extends Model { + @hasMany('comment', { async: true, inverse: 'post' }) + comments; + } + + class Comment extends Model { + @belongsTo('post', { async: true, inverse: 'comments' }) + post; + } + + register('model:post', Post); + register('model:comment', Comment); + + const comment = store.createRecord('comment'); + const post = store.createRecord('post'); + const comments = await post.comments; + comments.push(comment); + const identifier = recordIdentifierFor(comment); + + await comment.destroyRecord(); + + assert.false(graphFor(store).identifiers.has(identifier), 'relationships are cleared'); + assert.strictEqual( + store._instanceCache.peek({ identifier, bucket: 'resourceCache' }), + undefined, + 'The cache is destroyed' + ); + }); }); diff --git a/tests/main/tests/integration/relationships/one-to-many-test.js b/tests/main/tests/integration/relationships/one-to-many-test.js index 0f1e830af61..ce59fd8fa13 100644 --- a/tests/main/tests/integration/relationships/one-to-many-test.js +++ b/tests/main/tests/integration/relationships/one-to-many-test.js @@ -5,6 +5,7 @@ import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; module('integration/relationships/one_to_many_test - OneToMany relationships', function (hooks) { setupTest(hooks); @@ -1483,4 +1484,50 @@ module('integration/relationships/one_to_many_test - OneToMany relationships', f assert.strictEqual(user.accounts.length, 0, 'User does not have the account anymore'); assert.strictEqual(account.user, null, 'Account does not have the user anymore'); }); + + deprecatedTest( + 'createRecord updates inverse record array which has observers', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 3 }, + async function (assert) { + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + adapter.findAll = () => { + return { + data: [ + { + id: '2', + type: 'user', + attributes: { + name: 'Stanley', + }, + }, + ], + }; + }; + + const users = await store.findAll('user'); + assert.strictEqual(users.length, 1, 'Exactly 1 user'); + + const user = users.at(0); + assert.strictEqual(user.messages.length, 0, 'Record array is initially empty'); + + // set up an observer + user.addObserver('messages.@each.title', () => {}); + user.messages.objectAt(0); + + const messages = await user.messages; + + assert.strictEqual(messages.length, 0, 'we have no messages'); + assert.strictEqual(user.messages.length, 0, 'we have no messages'); + + const message = store.createRecord('message', { user, title: 'EmberFest was great' }); + assert.strictEqual(messages.length, 1, 'The message is added to the record array'); + assert.strictEqual(user.messages.length, 1, 'The message is added to the record array'); + + const messageFromArray = user.messages.objectAt(0); + assert.strictEqual(message, messageFromArray, 'Only one message record instance should be created'); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like', count: 3 }); + } + ); }); diff --git a/tests/main/tests/integration/relationships/one-to-one-test.js b/tests/main/tests/integration/relationships/one-to-one-test.js index a2ceff7fd8d..2487e22daa0 100644 --- a/tests/main/tests/integration/relationships/one-to-one-test.js +++ b/tests/main/tests/integration/relationships/one-to-one-test.js @@ -7,7 +7,9 @@ import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_PROMISE_PROXIES } from '@warp-drive/build-config/deprecations'; module('integration/relationships/one_to_one_test - OneToOne relationships', function (hooks) { setupTest(hooks); @@ -427,6 +429,109 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun assert.strictEqual(job.user, user, 'User relationship was set up correctly'); }); + deprecatedTest( + 'Setting a BelongsTo to a promise unwraps the promise before setting- async', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 1 }, + async function (assert) { + const store = this.owner.lookup('service:store'); + const stanley = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + bestFriend: { + data: { + id: '2', + type: 'user', + }, + }, + }, + }, + }); + const stanleysFriend = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: "Stanley's friend", + }, + }, + }); + const newFriend = store.push({ + data: { + id: '3', + type: 'user', + attributes: { + name: 'New friend', + }, + }, + }); + + newFriend.bestFriend = stanleysFriend.bestFriend; + const fetchedUser = await stanley.bestFriend; + assert.strictEqual( + fetchedUser, + newFriend, + `Stanley's bestFriend relationship was updated correctly to newFriend` + ); + const fetchedUser2 = await newFriend.bestFriend; + assert.strictEqual( + fetchedUser2, + stanley, + `newFriend's bestFriend relationship was updated correctly to be Stanley` + ); + } + ); + + deprecatedTest( + 'Setting a BelongsTo to a promise works when the promise returns null- async', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 1 }, + async function (assert) { + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + }, + }); + const igor = store.push({ + data: { + id: '2', + type: 'user', + attributes: { + name: 'Igor', + }, + }, + }); + const newFriend = store.push({ + data: { + id: '3', + type: 'user', + attributes: { + name: 'New friend', + }, + relationships: { + bestFriend: { + data: { + id: '1', + type: 'user', + }, + }, + }, + }, + }); + newFriend.bestFriend = igor.bestFriend; + const fetchedUser = await newFriend.bestFriend; + assert.strictEqual(fetchedUser, null, 'User relationship was updated correctly'); + } + ); + testInDebug("Setting a BelongsTo to a promise that didn't come from a relationship errors out", function (assert) { const store = this.owner.lookup('service:store'); @@ -457,11 +562,87 @@ module('integration/relationships/one_to_one_test - OneToOne relationships', fun }, }); - assert.expectAssertion(function () { - stanley.bestFriend = Promise.resolve(igor); - }, '[object Promise] is not a record instantiated by @ember-data/store'); + assert.expectAssertion( + function () { + stanley.bestFriend = Promise.resolve(igor); + }, + DEPRECATE_PROMISE_PROXIES + ? /You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo or hasMany relationship to the get call./ + : '[object Promise] is not a record instantiated by @ember-data/store' + ); }); + deprecatedTest( + 'Setting a BelongsTo to a promise multiple times is resistant to race conditions when the first set resolves quicker', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 2 }, + async function (assert) { + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + const stanley = store.push({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Stanley', + }, + relationships: { + bestFriend: { + data: { + id: '2', + type: 'user', + }, + }, + }, + }, + }); + const igor = store.push({ + data: { + id: '3', + type: 'user', + attributes: { + name: 'Igor', + }, + relationships: { + bestFriend: { + data: { + id: '5', + type: 'user', + }, + }, + }, + }, + }); + const newFriend = store.push({ + data: { + id: '7', + type: 'user', + attributes: { + name: 'New friend', + }, + }, + }); + + adapter.findRecord = function (store, type, id, snapshot) { + if (id === '5') { + return Promise.resolve({ data: { id: '5', type: 'user', attributes: { name: "Igor's friend" } } }); + } else if (id === '2') { + return Promise.resolve({ data: { id: '2', type: 'user', attributes: { name: "Stanley's friend" } } }); + } + }; + + const stanleyPromise = stanley.bestFriend; + const igorPromise = igor.bestFriend; + + await Promise.all([stanleyPromise, igorPromise]); + newFriend.bestFriend = stanleyPromise; + newFriend.bestFriend = igorPromise; + + const fetchedUser = await newFriend.bestFriend; + assert.strictEqual(fetchedUser?.name, "Igor's friend", 'User relationship was updated correctly'); + } + ); + test('Setting a OneToOne relationship to null reflects correctly on the other side - async', async function (assert) { const store = this.owner.lookup('service:store'); diff --git a/tests/main/tests/integration/relationships/polymorphic-mixins-belongs-to-test.js b/tests/main/tests/integration/relationships/polymorphic-mixins-belongs-to-test.js index 0d6675ba25b..8c4c41bb34b 100644 --- a/tests/main/tests/integration/relationships/polymorphic-mixins-belongs-to-test.js +++ b/tests/main/tests/integration/relationships/polymorphic-mixins-belongs-to-test.js @@ -8,6 +8,7 @@ import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@warp-drive/build-config/deprecations'; module( 'integration/relationships/polymorphic_mixins_belongs_to_test - Polymorphic belongsTo relationships with mixins', @@ -140,7 +141,9 @@ module( function () { user.bestMessage = video; }, - `No 'user' field exists on 'not-message'. To use this type in the polymorphic relationship 'user.bestMessage' the relationships schema definition for not-message should include: + DEPRECATE_NON_EXPLICIT_POLYMORPHISM + ? "The 'not-message' type does not implement 'message' and thus cannot be assigned to the 'bestMessage' relationship in 'user'. Make it a descendant of 'message' or use a mixin of the same name." + : `No 'user' field exists on 'not-message'. To use this type in the polymorphic relationship 'user.bestMessage' the relationships schema definition for not-message should include: \`\`\` { diff --git a/tests/main/tests/integration/relationships/polymorphic-mixins-has-many-test.js b/tests/main/tests/integration/relationships/polymorphic-mixins-has-many-test.js index 335a6cdca2e..7b9ebfad940 100644 --- a/tests/main/tests/integration/relationships/polymorphic-mixins-has-many-test.js +++ b/tests/main/tests/integration/relationships/polymorphic-mixins-has-many-test.js @@ -8,6 +8,7 @@ import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_NON_EXPLICIT_POLYMORPHISM } from '@warp-drive/build-config/deprecations'; module( 'integration/relationships/polymorphic_mixins_has_many_test - Polymorphic hasMany relationships with mixins', @@ -183,12 +184,9 @@ module( ], }); - const fetchedMessages = await user.messages; - assert.expectAssertion( - function () { - fetchedMessages.push(notMessage); - }, - `No 'user' field exists on 'not-message'. To use this type in the polymorphic relationship 'user.messages' the relationships schema definition for not-message should include: + const expectedError = DEPRECATE_NON_EXPLICIT_POLYMORPHISM + ? /The 'not-message' type does not implement 'message' and thus cannot be assigned to the 'messages' relationship in 'user'. Make it a descendant of 'message/ + : `No 'user' field exists on 'not-message'. To use this type in the polymorphic relationship 'user.messages' the relationships schema definition for not-message should include: \`\`\` { @@ -206,7 +204,15 @@ module( } \`\`\` -` +`; + + const fetchedMessages = await user.messages; + assert.expectAssertion( + function () { + fetchedMessages.push(notMessage); + }, + expectedError, + `expected an error to match ${expectedError}` ); } ); diff --git a/tests/main/tests/integration/relationships/promise-many-array-test.js b/tests/main/tests/integration/relationships/promise-many-array-test.js new file mode 100644 index 00000000000..1472d937c2b --- /dev/null +++ b/tests/main/tests/integration/relationships/promise-many-array-test.js @@ -0,0 +1,154 @@ +import { A } from '@ember/array'; +import EmberObject, { computed } from '@ember/object'; +import { filterBy } from '@ember/object/computed'; +import { settled } from '@ember/test-helpers'; + +import { module } from 'qunit'; + +import { setupRenderingTest } from 'ember-qunit'; + +import Model, { attr, hasMany } from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; +import { DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS } from '@warp-drive/build-config/deprecations'; + +module('PromiseManyArray', (hooks) => { + setupRenderingTest(hooks); + + deprecatedTest( + 'PromiseManyArray is not side-affected by EmberArray', + { id: 'ember-data:no-a-with-array-like', until: '5.0', count: 1 }, + async function (assert) { + const { owner } = this; + class Person extends Model { + @attr('string') name; + } + class Group extends Model { + @hasMany('person', { async: true, inverse: null }) members; + } + owner.register('model:person', Person); + owner.register('model:group', Group); + const store = owner.lookup('service:store'); + const members = ['Bob', 'John', 'Michael', 'Larry', 'Lucy'].map((name) => store.createRecord('person', { name })); + const group = store.createRecord('group', { members }); + + const forEachFn = group.members.forEach; + assert.strictEqual(group.members.length, 5, 'initial length is correct'); + + if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) { + group.members.replace(0, 1); + assert.strictEqual(group.members.length, 4, 'updated length is correct'); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + } + + A(group.members); + + assert.strictEqual(forEachFn, group.members.forEach, 'we have the same function for forEach'); + + if (DEPRECATE_PROMISE_MANY_ARRAY_BEHAVIORS) { + group.members.replace(0, 1); + assert.strictEqual(group.members.length, 3, 'updated length is correct'); + // we'll want to use a different test for this but will want to still ensure we are not side-affected + assert.expectDeprecation({ id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 2 }); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + } + } + ); + + deprecatedTest( + 'PromiseManyArray can be subscribed to by computed chains', + { id: 'ember-data:deprecate-promise-many-array-behaviors', until: '5.0', count: 16 }, + async function (assert) { + const { owner } = this; + class Person extends Model { + @attr('string') name; + } + class Group extends Model { + @hasMany('person', { async: true, inverse: null }) members; + + @computed('members.@each.id') + get memberIds() { + return this.members.map((m) => m.id); + } + + @filterBy('members', 'name', 'John') + johns; + } + owner.register('model:person', Person); + owner.register('model:group', Group); + owner.register( + 'serializer:application', + class extends EmberObject { + normalizeResponse(_, __, data) { + return data; + } + } + ); + + let _id = 0; + const names = ['Bob', 'John', 'Michael', 'John', 'Larry', 'Lucy']; + owner.register( + 'adapter:application', + class extends EmberObject { + findRecord(_store, _schema, id) { + assert.step(`findRecord ${id}`); + assert.strictEqual(id, String(_id + 1), 'findRecord id is correct'); + const name = names[_id++]; + const data = { + type: 'person', + id: `${_id}`, + attributes: { + name, + }, + }; + return { data }; + } + } + ); + const store = owner.lookup('service:store'); + + const group = store.push({ + data: { + type: 'group', + id: '1', + relationships: { + members: { + data: [ + { type: 'person', id: '1' }, + { type: 'person', id: '2' }, + { type: 'person', id: '3' }, + { type: 'person', id: '4' }, + { type: 'person', id: '5' }, + { type: 'person', id: '6' }, + ], + }, + }, + }, + }); + + // access the group data + let memberIds = group.memberIds; + let johnRecords = group.johns; + assert.strictEqual(memberIds.length, 0, 'member ids is 0 initially'); + assert.strictEqual(johnRecords.length, 0, 'john ids is 0 initially'); + + await settled(); + + assert.verifySteps([ + 'findRecord 1', + 'findRecord 2', + 'findRecord 3', + 'findRecord 4', + 'findRecord 5', + 'findRecord 6', + ]); + + memberIds = group.memberIds; + johnRecords = group.johns; + assert.strictEqual(memberIds.length, 6, 'memberIds length is correct'); + assert.strictEqual(johnRecords.length, 2, 'johnRecords length is correct'); + assert.strictEqual(group.members.length, 6, 'members length is correct'); + assert.expectDeprecation({ id: 'ember-data:no-a-with-array-like', count: 2 }); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like', count: 12 }); + } + ); +}); diff --git a/tests/main/tests/integration/snapshot-test.js b/tests/main/tests/integration/snapshot-test.js index 191e9d66891..8accd4572ef 100644 --- a/tests/main/tests/integration/snapshot-test.js +++ b/tests/main/tests/integration/snapshot-test.js @@ -6,6 +6,7 @@ import JSONAPIAdapter from '@ember-data/adapter/json-api'; import { FetchManager, Snapshot } from '@ember-data/legacy-compat/-private'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; let owner, store; @@ -108,6 +109,45 @@ module('integration/snapshot - Snapshot', function (hooks) { assert.strictEqual(snapshot.modelName, 'post', 'modelName is correct'); }); + deprecatedTest( + 'snapshot.type loads the class lazily', + { + id: 'ember-data:deprecate-snapshot-model-class-access', + count: 1, + until: '5.0', + }, + async function (assert) { + assert.expect(3); + + let postClassLoaded = false; + const modelFor = store.modelFor; + store.modelFor = (name) => { + if (name === 'post') { + postClassLoaded = true; + } + return modelFor.call(store, name); + }; + + await store._push({ + data: { + type: 'post', + id: '1', + attributes: { + title: 'Hello World', + }, + }, + }); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'post', id: '1' }); + const snapshot = await store._fetchManager.createSnapshot(identifier); + + assert.false(postClassLoaded, 'model class is not eagerly loaded'); + const type = snapshot.type; + assert.true(postClassLoaded, 'model class is loaded'); + const Post = store.modelFor('post'); + assert.strictEqual(type, Post, 'type is correct'); + } + ); + test('an initial findRecord call has no record for internal-model when a snapshot is generated', async function (assert) { assert.expect(2); store.adapterFor('application').findRecord = (store, type, id, snapshot) => { diff --git a/tests/main/tests/integration/store/adapter-for-test.js b/tests/main/tests/integration/store/adapter-for-test.js index d5573ba97d0..785785670ff 100644 --- a/tests/main/tests/integration/store/adapter-for-test.js +++ b/tests/main/tests/integration/store/adapter-for-test.js @@ -5,6 +5,8 @@ import { module, test } from 'qunit'; import Store from 'ember-data/store'; import { setupTest } from 'ember-qunit'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; + class TestAdapter { constructor(args) { Object.assign(this, args); @@ -158,6 +160,59 @@ module('integration/store - adapterFor', function (hooks) { assert.strictEqual(appAdapter, adapter, 'We fell back to the application adapter instance'); }); + deprecatedTest( + 'When the per-type, application and specified fallback adapters do not exist, we fallback to the -json-api adapter', + { + id: 'ember-data:deprecate-secret-adapter-fallback', + until: '5.0', + count: 2, + }, + async function (assert) { + const { owner } = this; + + let didInstantiateAdapter = false; + + class JsonApiAdapter extends TestAdapter { + didInit() { + didInstantiateAdapter = true; + } + } + + const lookup = owner.lookup; + owner.lookup = (registrationName) => { + if (registrationName === 'adapter:application') { + return undefined; + } + return lookup.call(owner, registrationName); + }; + + owner.unregister('adapter:-json-api'); + owner.register('adapter:-json-api', JsonApiAdapter); + + const adapter = store.adapterFor('person'); + + assert.ok(adapter instanceof JsonApiAdapter, 'We found the adapter'); + assert.ok(didInstantiateAdapter, 'We instantiated the adapter'); + didInstantiateAdapter = false; + + const appAdapter = store.adapterFor('application'); + + assert.ok(appAdapter instanceof JsonApiAdapter, 'We found the fallback -json-api adapter for application'); + assert.notOk(didInstantiateAdapter, 'We did not instantiate the adapter again'); + didInstantiateAdapter = false; + + const jsonApiAdapter = store.adapterFor('-json-api'); + assert.ok(jsonApiAdapter instanceof JsonApiAdapter, 'We found the correct adapter'); + assert.notOk(didInstantiateAdapter, 'We did not instantiate the adapter again'); + assert.strictEqual(jsonApiAdapter, appAdapter, 'We fell back to the -json-api adapter instance for application'); + assert.strictEqual( + jsonApiAdapter, + adapter, + 'We fell back to the -json-api adapter instance for the per-type adapter' + ); + } + ); + test('adapters are destroyed', async function (assert) { const { owner } = this; let didInstantiate = false; diff --git a/tests/main/tests/integration/store/query-test.js b/tests/main/tests/integration/store/query-test.js new file mode 100644 index 00000000000..f705cdd472a --- /dev/null +++ b/tests/main/tests/integration/store/query-test.js @@ -0,0 +1,49 @@ +import { module } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import Adapter from '@ember-data/adapter'; +import Model from '@ember-data/model'; +import { createDeferred } from '@ember-data/request'; +import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; + +module('integration/store/query', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + class Person extends Model {} + + this.owner.register('model:person', Person); + this.owner.register('adapter:application', Adapter); + this.owner.register('serializer:application', class extends JSONAPISerializer {}); + }); + + deprecatedTest( + 'meta is proxied correctly on the PromiseArray', + { id: 'ember-data:deprecate-promise-proxies', until: '5.0', count: 2 }, + async function (assert) { + const store = this.owner.lookup('service:store'); + + const defered = createDeferred(); + + this.owner.register( + 'adapter:person', + class extends Adapter { + query(store, type, query) { + return defered.promise; + } + } + ); + + const result = store.query('person', {}); + + assert.notOk(result.meta?.foo, 'precond: meta is not yet set'); + + defered.resolve({ data: [], meta: { foo: 'bar' } }); + await result; + + assert.strictEqual(result.meta?.foo, 'bar', 'meta is now proxied'); + } + ); +}); diff --git a/tests/main/tests/unit/adapter-errors-test.js b/tests/main/tests/unit/adapter-errors-test.js index a5db6d55b6c..356d7979bfb 100644 --- a/tests/main/tests/unit/adapter-errors-test.js +++ b/tests/main/tests/unit/adapter-errors-test.js @@ -3,6 +3,8 @@ import { module, test } from 'qunit'; import AdapterError, { AbortError, ConflictError, + errorsArrayToHash, + errorsHashToArray, ForbiddenError, InvalidError, NotFoundError, @@ -11,6 +13,7 @@ import AdapterError, { UnauthorizedError, } from '@ember-data/adapter/error'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; +import { DEPRECATE_HELPERS } from '@warp-drive/build-config/deprecations'; module('unit/adapter-errors - AdapterError', function () { test('AdapterError', function (assert) { @@ -110,6 +113,83 @@ module('unit/adapter-errors - AdapterError', function () { assert.strictEqual(error.message, 'custom error!'); }); + if (DEPRECATE_HELPERS) { + const errorsHash = { + name: ['is invalid', 'must be a string'], + age: ['must be a number'], + }; + + const errorsArray = [ + { + title: 'Invalid Attribute', + detail: 'is invalid', + source: { pointer: '/data/attributes/name' }, + }, + { + title: 'Invalid Attribute', + detail: 'must be a string', + source: { pointer: '/data/attributes/name' }, + }, + { + title: 'Invalid Attribute', + detail: 'must be a number', + source: { pointer: '/data/attributes/age' }, + }, + ]; + + const errorsPrimaryHash = { + base: ['is invalid', 'error message'], + }; + + const errorsPrimaryArray = [ + { + title: 'Invalid Document', + detail: 'is invalid', + source: { pointer: '/data' }, + }, + { + title: 'Invalid Document', + detail: 'error message', + source: { pointer: '/data' }, + }, + ]; + + test('errorsHashToArray', function (assert) { + const result = errorsHashToArray(errorsHash); + assert.deepEqual(result, errorsArray); + assert.expectDeprecation({ id: 'ember-data:deprecate-errors-hash-to-array-helper', count: 1 }); + }); + + test('errorsHashToArray for primary data object', function (assert) { + const result = errorsHashToArray(errorsPrimaryHash); + assert.deepEqual(result, errorsPrimaryArray); + assert.expectDeprecation({ id: 'ember-data:deprecate-errors-hash-to-array-helper', count: 1 }); + }); + + test('errorsArrayToHash', function (assert) { + const result = errorsArrayToHash(errorsArray); + assert.deepEqual(result, errorsHash); + assert.expectDeprecation({ id: 'ember-data:deprecate-errors-array-to-hash-helper', count: 1 }); + }); + + test('errorsArrayToHash without trailing slash', function (assert) { + const result = errorsArrayToHash([ + { + detail: 'error message', + source: { pointer: 'data/attributes/name' }, + }, + ]); + assert.deepEqual(result, { name: ['error message'] }); + assert.expectDeprecation({ id: 'ember-data:deprecate-errors-array-to-hash-helper', count: 1 }); + }); + + test('errorsArrayToHash for primary data object', function (assert) { + const result = errorsArrayToHash(errorsPrimaryArray); + assert.deepEqual(result, errorsPrimaryHash); + assert.expectDeprecation({ id: 'ember-data:deprecate-errors-array-to-hash-helper', count: 1 }); + }); + } + testInDebug('InvalidError will normalize errors hash will assert', function (assert) { assert.expectAssertion(function () { new InvalidError({ name: ['is invalid'] }); diff --git a/tests/main/tests/unit/model/relationships-test.js b/tests/main/tests/unit/model/relationships-test.js index 988d410df99..6cceea0bde9 100644 --- a/tests/main/tests/unit/model/relationships-test.js +++ b/tests/main/tests/unit/model/relationships-test.js @@ -5,6 +5,7 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import Model, { belongsTo, hasMany } from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; class Person extends Model { @hasMany('occupation', { async: false, inverse: null }) occupations; @@ -126,4 +127,36 @@ module('[@ember-data/model] unit - relationships', function (hooks) { assert.strictEqual(relationship.name, 'streamItems', 'relationship name has not been changed'); }); + + deprecatedTest( + 'decorators works without parens', + { id: 'ember-data:deprecate-non-strict-relationships', until: '5.0', count: 6 }, + function (assert) { + const { owner } = this; + + class StreamItem extends Model { + @belongsTo user; + } + + class User extends Model { + @hasMany streamItems; + } + + owner.unregister('model:user'); + owner.register('model:stream-item', StreamItem); + owner.register('model:user', User); + + const store = owner.lookup('service:store'); + + const user = store.modelFor('user'); + + const relationships = get(user, 'relationships'); + + assert.ok(relationships.has('stream-item'), 'relationship key has been normalized'); + + const relationship = relationships.get('stream-item')[0]; + + assert.strictEqual(relationship.name, 'streamItems', 'relationship name has not been changed'); + } + ); }); diff --git a/tests/main/tests/unit/model/relationships/has-many-test.js b/tests/main/tests/unit/model/relationships/has-many-test.js index a5e76f97a2c..39444d0ec81 100644 --- a/tests/main/tests/unit/model/relationships/has-many-test.js +++ b/tests/main/tests/unit/model/relationships/has-many-test.js @@ -12,6 +12,7 @@ import { recordIdentifierFor } from '@ember-data/store'; import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; import todo from '@ember-data/unpublished-test-infra/test-support/todo'; +import { DEPRECATE_ARRAY_LIKE, DEPRECATE_MANY_ARRAY_DUPLICATES } from '@warp-drive/build-config/deprecations'; module('unit/model/relationships - hasMany', function (hooks) { setupTest(hooks); @@ -2105,7 +2106,7 @@ module('unit/model/relationships - hasMany', function (hooks) { ); test('possible to replace items in a relationship using setObjects w/ Ember Enumerable Array/Object as the argument (GH-2533)', function (assert) { - assert.expect(2); + assert.expect(DEPRECATE_ARRAY_LIKE ? 3 : 2); const Tag = Model.extend({ name: attr('string'), @@ -2169,13 +2170,78 @@ module('unit/model/relationships - hasMany', function (hooks) { const sylvain = store.peekRecord('person', '2'); // Test that since sylvain.tags instanceof ManyArray, // adding records on Relationship iterates correctly. - tom.tags.length = 0; - tom.tags.push(...sylvain.tags); + if (DEPRECATE_ARRAY_LIKE) { + tom.tags.setObjects(sylvain.tags); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + } else { + tom.tags.length = 0; + tom.tags.push(...sylvain.tags); + } assert.strictEqual(tom.tags.length, 1); assert.strictEqual(tom.tags.at(0), store.peekRecord('tag', 2)); }); + deprecatedTest( + 'Replacing `has-many` with non-array will throw assertion', + { id: 'ember-data:deprecate-array-like', until: '5.0' }, + function (assert) { + assert.expect(1); + + const Tag = Model.extend({ + name: attr('string'), + person: belongsTo('person', { async: false, inverse: 'tags' }), + }); + + const Person = Model.extend({ + name: attr('string'), + tags: hasMany('tag', { async: false, inverse: 'person' }), + }); + + this.owner.register('model:tag', Tag); + this.owner.register('model:person', Person); + + const store = this.owner.lookup('service:store'); + + store.push({ + data: [ + { + type: 'person', + id: '1', + attributes: { + name: 'Tom Dale', + }, + relationships: { + tags: { + data: [{ type: 'tag', id: '1' }], + }, + }, + }, + { + type: 'tag', + id: '1', + attributes: { + name: 'ember', + }, + }, + { + type: 'tag', + id: '2', + attributes: { + name: 'ember-data', + }, + }, + ], + }); + + const tom = store.peekRecord('person', '1'); + const tag = store.peekRecord('tag', '2'); + assert.expectAssertion(() => { + tom.tags.setObjects(tag); + }, /ManyArray.setObjects expects to receive an array as its argument/); + } + ); + test('it is possible to remove an item from a relationship', async function (assert) { assert.expect(2); @@ -2726,4 +2792,43 @@ module('unit/model/relationships - hasMany', function (hooks) { await settled(); }); + + test('checks if passed array only contains instances of Model', async function (assert) { + class Person extends Model { + @attr name; + } + class Tag extends Model { + @hasMany('person', { async: true, inverse: null }) people; + } + + this.owner.register('model:tag', Tag); + this.owner.register('model:person', Person); + + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); + + adapter.findRecord = function () { + return { + data: { + type: 'person', + id: '1', + }, + }; + }; + + const tag = store.createRecord('tag'); + const person = store.findRecord('person', '1'); + await person; + + tag.people = [person]; + + assert.expectAssertion(() => { + tag.people = [person, {}]; + }, /All elements of a hasMany relationship must be instances of Model/); + assert.expectDeprecation({ + id: 'ember-data:deprecate-promise-proxies', + count: /* inline-macro-config */ DEPRECATE_MANY_ARRAY_DUPLICATES ? 5 : 4, + until: '5.0', + }); + }); }); diff --git a/tests/main/tests/unit/record-arrays/adapter-populated-record-array-test.js b/tests/main/tests/unit/record-arrays/adapter-populated-record-array-test.js index e062ef640ab..3ddab68d8de 100644 --- a/tests/main/tests/unit/record-arrays/adapter-populated-record-array-test.js +++ b/tests/main/tests/unit/record-arrays/adapter-populated-record-array-test.js @@ -50,6 +50,18 @@ module('unit/record-arrays/collection', function (hooks) { assert.strictEqual(recordArray.links, 'foo'); }); + testInDebug('#replace() throws error', function (assert) { + const recordArray = new CollectionRecordArray({ type: 'recordType', identifiers: [] }); + + assert.throws( + () => { + recordArray.replace(); + }, + Error('Mutating this array of records via splice is not allowed.'), + 'throws error' + ); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + }); testInDebug('mutation throws error', function (assert) { const recordArray = new CollectionRecordArray({ type: 'recordType', identifiers: [] }); diff --git a/tests/main/tests/unit/record-arrays/record-array-test.js b/tests/main/tests/unit/record-arrays/record-array-test.js index bd24c73088f..56dc55d4989 100644 --- a/tests/main/tests/unit/record-arrays/record-array-test.js +++ b/tests/main/tests/unit/record-arrays/record-array-test.js @@ -7,6 +7,7 @@ import Model, { attr } from '@ember-data/model'; import { createDeferred } from '@ember-data/request'; import { recordIdentifierFor } from '@ember-data/store'; import { LiveArray, SOURCE } from '@ember-data/store/-private'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; class Tag extends Model { @@ -39,6 +40,19 @@ module('unit/record-arrays/live-array - LiveArray', function (hooks) { assert.strictEqual(recordArray.store, store); }); + testInDebug('#replace() throws error', async function (assert) { + const recordArray = new LiveArray({ identifiers: [], type: 'recordType' }); + + assert.throws( + () => { + recordArray.replace(); + }, + Error('Mutating this array of records via splice is not allowed.'), + 'throws error' + ); + assert.expectDeprecation({ id: 'ember-data:deprecate-array-like' }); + }); + testInDebug('Mutation throws error', async function (assert) { const recordArray = new LiveArray({ identifiers: [], type: 'recordType' }); @@ -85,6 +99,219 @@ module('unit/record-arrays/live-array - LiveArray', function (hooks) { assert.strictEqual(recordArray[3], undefined); }); + deprecatedTest( + '#filterBy', + { id: 'ember-data:deprecate-array-like', until: '5.0', count: 3 }, + async function (assert) { + this.owner.register('model:tag', Tag); + const store = this.owner.lookup('service:store'); + + const records = store.push({ + data: [ + { + type: 'tag', + id: '1', + attributes: { + name: 'first', + }, + }, + { + type: 'tag', + id: '3', + }, + { + type: 'tag', + id: '5', + attributes: { + name: 'fifth', + }, + }, + ], + }); + + const recordArray = new LiveArray({ + type: 'recordType', + identifiers: records.map(recordIdentifierFor), + store, + }); + + assert.strictEqual(recordArray.length, 3); + assert.strictEqual(recordArray.filterBy('id', '3').length, 1); + assert.strictEqual(recordArray.filterBy('id').length, 3); + assert.strictEqual(recordArray.filterBy('name').length, 2); + } + ); + + deprecatedTest('#reject', { id: 'ember-data:deprecate-array-like', until: '5.0', count: 3 }, async function (assert) { + this.owner.register('model:tag', Tag); + const store = this.owner.lookup('service:store'); + + const records = store.push({ + data: [ + { + type: 'tag', + id: '1', + attributes: { + name: 'first', + }, + }, + { + type: 'tag', + id: '3', + }, + { + type: 'tag', + id: '5', + attributes: { + name: 'fifth', + }, + }, + ], + }); + + const recordArray = new LiveArray({ + type: 'recordType', + identifiers: records.map(recordIdentifierFor), + store, + }); + + assert.strictEqual(recordArray.length, 3); + assert.strictEqual(recordArray.reject(({ id }) => id === '3').length, 2); + assert.strictEqual(recordArray.reject(({ id }) => id).length, 0); + assert.strictEqual(recordArray.reject(({ name }) => name).length, 1); + }); + + deprecatedTest( + '#rejectBy', + { id: 'ember-data:deprecate-array-like', until: '5.0', count: 3 }, + async function (assert) { + this.owner.register('model:tag', Tag); + const store = this.owner.lookup('service:store'); + + const records = store.push({ + data: [ + { + type: 'tag', + id: '1', + attributes: { + name: 'first', + }, + }, + { + type: 'tag', + id: '3', + }, + { + type: 'tag', + id: '5', + attributes: { + name: 'fifth', + }, + }, + ], + }); + + const recordArray = new LiveArray({ + type: 'recordType', + identifiers: records.map(recordIdentifierFor), + store, + }); + + assert.strictEqual(recordArray.length, 3); + assert.strictEqual(recordArray.rejectBy('id', '3').length, 2); + assert.strictEqual(recordArray.rejectBy('id').length, 0); + assert.strictEqual(recordArray.rejectBy('name').length, 1); + } + ); + + deprecatedTest( + '#lastObject and #firstObject', + { id: 'ember-data:deprecate-array-like', until: '5.0', count: 2 }, + async function (assert) { + this.owner.register('model:tag', Tag); + const store = this.owner.lookup('service:store'); + + const records = store.push({ + data: [ + { + type: 'tag', + id: '1', + attributes: { + name: 'first', + }, + }, + { + type: 'tag', + id: '3', + }, + { + type: 'tag', + id: '5', + attributes: { + name: 'fifth', + }, + }, + ], + }); + + const recordArray = new LiveArray({ + type: 'recordType', + identifiers: records.map(recordIdentifierFor), + store, + }); + + assert.strictEqual(recordArray.length, 3); + assert.strictEqual(recordArray.firstObject.id, '1'); + assert.strictEqual(recordArray.lastObject.id, '5'); + } + ); + + deprecatedTest( + '#objectAt and #objectsAt', + { id: 'ember-data:deprecate-array-like', until: '5.0', count: 5 }, + async function (assert) { + this.owner.register('model:tag', Tag); + const store = this.owner.lookup('service:store'); + + const records = store.push({ + data: [ + { + type: 'tag', + id: '1', + attributes: { + name: 'first', + }, + }, + { + type: 'tag', + id: '3', + }, + { + type: 'tag', + id: '5', + attributes: { + name: 'fifth', + }, + }, + ], + }); + + const recordArray = new LiveArray({ + type: 'recordType', + identifiers: records.map(recordIdentifierFor), + store, + }); + + assert.strictEqual(recordArray.length, 3); + assert.strictEqual(recordArray.objectAt(0).id, '1'); + assert.strictEqual(recordArray.objectAt(-1).id, '5'); + assert.deepEqual( + recordArray.objectsAt([2, 1]).map((r) => r.id), + ['5', '3'] + ); + } + ); + test('#update', async function (assert) { let findAllCalled = 0; const deferred = createDeferred(); diff --git a/tests/main/tests/unit/store/adapter-interop-test.js b/tests/main/tests/unit/store/adapter-interop-test.js index 80c50987b47..29585f9502d 100644 --- a/tests/main/tests/unit/store/adapter-interop-test.js +++ b/tests/main/tests/unit/store/adapter-interop-test.js @@ -1225,4 +1225,18 @@ module('unit/store/adapter-interop - Store working with a Adapter', function (ho await settled(); assert.strictEqual(store.peekRecord('person', 1).name, 'Tom', 'after background reload name is loaded'); }); + + testInDebug('Calling adapterFor with a model class should assert', function (assert) { + const Person = Model.extend(); + + this.owner.register('model:person', Person); + + const store = this.owner.lookup('service:store'); + + assert.expectAssertion(() => { + store.adapterFor(Person); + }, /Passing classes to store.adapterFor has been removed/); + + assert.expectDeprecation({ id: 'ember-data:deprecate-early-static' }); + }); }); diff --git a/tests/main/tests/unit/store/serializer-for-test.js b/tests/main/tests/unit/store/serializer-for-test.js index e7a439b1889..8ce56163706 100644 --- a/tests/main/tests/unit/store/serializer-for-test.js +++ b/tests/main/tests/unit/store/serializer-for-test.js @@ -4,6 +4,7 @@ import { setupTest } from 'ember-qunit'; import Model from '@ember-data/model'; import JSONSerializer from '@ember-data/serializer/json'; +import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; let store, Person; @@ -39,4 +40,12 @@ module('unit/store/serializer_for - Store#serializerFor', function (hooks) { 'serializer returned from serializerFor is an instance of ApplicationSerializer' ); }); + + testInDebug('Calling serializerFor with a model class should assert', function (assert) { + assert.expectAssertion(() => { + store.serializerFor(Person); + }, /Passing classes to store.serializerFor has been removed/); + + assert.expectDeprecation({ id: 'ember-data:deprecate-early-static' }); + }); }); diff --git a/tests/main/tests/unit/system/snapshot-record-array-test.js b/tests/main/tests/unit/system/snapshot-record-array-test.js index 2bd81d943f3..0ca3f94b149 100644 --- a/tests/main/tests/unit/system/snapshot-record-array-test.js +++ b/tests/main/tests/unit/system/snapshot-record-array-test.js @@ -6,6 +6,7 @@ import { setupTest } from 'ember-qunit'; import { FetchManager, SnapshotRecordArray } from '@ember-data/legacy-compat/-private'; import Model, { attr } from '@ember-data/model'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; module('Unit - snapshot-record-array', function (hooks) { setupTest(hooks); @@ -74,4 +75,43 @@ module('Unit - snapshot-record-array', function (hooks) { assert.strictEqual(snapshot.snapshots()[0], snapshotsTaken[0], 'should return the exact same snapshot'); assert.strictEqual(didTakeSnapshot, 1, 'still only one snapshot should have been taken'); }); + + deprecatedTest( + 'SnapshotRecordArray.type loads the class lazily', + { + id: 'ember-data:deprecate-snapshot-model-class-access', + count: 1, + until: '5.0', + }, + function (assert) { + const array = A([1, 2]); + let typeLoaded = false; + + Object.defineProperty(array, 'type', { + get() { + typeLoaded = true; + return 'some type'; + }, + }); + + const options = { + adapterOptions: 'some options', + include: 'include me', + }; + + const snapshot = new SnapshotRecordArray( + { + peekAll() { + return array; + }, + }, + 'user', + options + ); + + assert.false(typeLoaded, 'model class is not eager loaded'); + assert.strictEqual(snapshot.type, 'some type'); + assert.true(typeLoaded, 'model class is loaded'); + } + ); }); diff --git a/tests/main/tsconfig.json b/tests/main/tsconfig.json index 32a05641fb9..5bf99cce967 100644 --- a/tests/main/tsconfig.json +++ b/tests/main/tsconfig.json @@ -53,8 +53,6 @@ "@ember-data/unpublished-test-infra/*": ["../../packages/unpublished-test-infra/unstable-preview-types/*"], "@warp-drive/core-types": ["../../packages/core-types/unstable-preview-types"], "@warp-drive/core-types/*": ["../../packages/core-types/unstable-preview-types/*"], - "@warp-drive/schema-record": ["../../packages/schema-record/unstable-preview-types"], - "@warp-drive/schema-record/*": ["../../packages/schema-record/unstable-preview-types/*"], "@warp-drive/build-config": ["../../packages/build-config/unstable-preview-types"], "@warp-drive/build-config/*": ["../../packages/build-config/unstable-preview-types/*"], "@warp-drive/holodeck": ["../../packages/holodeck/unstable-preview-types"], @@ -111,9 +109,6 @@ }, { "path": "../../packages/-ember-data" - }, - { - "path": "../../packages/schema-record" } ] } diff --git a/tests/performance/package.json b/tests/performance/package.json index 5d7440724fb..4a551b41c0f 100644 --- a/tests/performance/package.json +++ b/tests/performance/package.json @@ -1,6 +1,6 @@ { "name": "performance-test-app", - "version": "5.4.0-alpha.121", + "version": "4.12.8", "private": true, "description": "Small description for performance-test-app goes here", "repository": { diff --git a/tests/vite-basic-compat/package.json b/tests/vite-basic-compat/package.json index af5fad2de7f..140bd1e2109 100644 --- a/tests/vite-basic-compat/package.json +++ b/tests/vite-basic-compat/package.json @@ -1,6 +1,6 @@ { "name": "vite-basic-compat", - "version": "0.0.1-alpha.4", + "version": "4.12.8", "private": true, "description": "Small description for vite-basic-compat goes here", "repository": "", diff --git a/tsconfig.json b/tsconfig.json index e36abab2bd7..f102572daee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,6 @@ { "path": "./packages/core-types" }, { "path": "./packages/debug" }, { "path": "./packages/diagnostic" }, - // { "path": "./packages/ember" }, { "path": "./packages/graph" }, { "path": "./packages/holodeck" }, { "path": "./packages/json-api" }, @@ -21,11 +20,9 @@ { "path": "./packages/request" }, { "path": "./packages/request-utils" }, { "path": "./packages/rest" }, - { "path": "./packages/schema-record" }, { "path": "./packages/serializer" }, { "path": "./packages/store" }, { "path": "./packages/tracking" }, - { "path": "./packages/unpublished-test-infra" }, - { "path": "./packages/experiments" } + { "path": "./packages/unpublished-test-infra" } ] }