From 47eba4985dcdda41d48d3d266118cee67dd51f68 Mon Sep 17 00:00:00 2001 From: chen21019 <19357113+chen21019@users.noreply.github.com> Date: Sat, 3 Oct 2026 16:37:06 +0800 Subject: [PATCH] fix: preserve API-key first delivery with exact validated Web172 tree --- COMPATIBILITY.md | 14 ++ README.md | 13 ++ app/components/edit-apikey/component.js | 3 +- app/mixins/new-or-edit.js | 35 ++++ ...ass-replacement.node24-ignore-scripts.json | 10 +- docs/releases/web-console-1.6.171.md | 1 - docs/releases/web-console-1.6.172.md | 65 ++++++ package-lock.json | 10 +- package.json | 4 +- scripts/check-modernization-blockers | 4 +- scripts/check-ui-console-workspace | 2 +- scripts/check-ui-critical-high-dependencies | 4 +- .../check-ui-ember-api-store-fetch-upgrade | 20 +- scripts/node24-lock-smoke.js | 13 +- tests/unit/mixins/new-or-edit-test.js | 74 +++++++ .../vendor/api-store-create-order-test.js | 188 +++++++++++++++++- vendor/ember-api-store-compat/UPSTREAM.md | 10 + .../addon/mixins/type.js | 12 +- .../addon/services/store.js | 36 ++++ .../addon/utils/create-only-delivery.js | 36 ++++ .../ember-api-store-2.8.5-pasturestack.6.tgz | Bin 0 -> 24320 bytes vendor/ember-api-store-compat/package.json | 2 +- 22 files changed, 513 insertions(+), 43 deletions(-) create mode 100644 docs/releases/web-console-1.6.172.md create mode 100644 vendor/ember-api-store-compat/addon/utils/create-only-delivery.js create mode 100644 vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.6.tgz diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 17a97728f0..697a7f11c3 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -4,6 +4,20 @@ Web Console preserves compatible API paths, schema and resource names, action na Visible branding, product-owned assets, icon identifiers, package metadata, and operator documentation use PastureStack. Historical identifiers remain only where they are server data or protocol contracts and must not be mechanically replaced. +Candidate `1.6.172` adds opt-in first delivery of fields whose actual Schema +declares `readOnCreateOnly: true`. Only `edit-apikey` enables it. A nonenumerable +request-private callback delivers the successful create values once to a +detached clone, not to serialized metadata or canonical cache. Matching Store, +generation, API base, opaque generated ID, concrete type and owner are required; +newer subscribe state and nested-resource adoption are preserved. Other +NewOrEdit hook arguments/results and consumers retain their previous contracts. +The save owner clears its own pending delivery on success and failure; rejected +duplicates cannot clear another save's lock or values. Compatibility revision 6 +is a new archive with the same dependency graph. Source/package checks pass; +local Chrome tests have not run because of incomplete shared dependencies. +Official tests, publication and packaged native acceptance are pending, not +full-matrix PASS. See the [release note](docs/releases/web-console-1.6.172.md). + Published `1.6.171` confines create-response adoption to ID-less POST/201 and an existing exact-ID/concrete-type canonical model in the same Store, generation and API base. It does not re-import stale scalar or nested create fields over diff --git a/README.md b/README.md index 31f16317e5..8bcc81ca37 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,19 @@ PastureStack is an independent community effort to preserve, audit, and moderniz ## Project status +Candidate `1.6.172` preserves API-key create-only first delivery when a redacted +subscribe model arrives before POST/201. Only the API-key editor opts into a +request-private, Schema-bound delivery to its detached clone; newer canonical +state and nested resources remain intact, and the canonical Store does not need +to retain the secret. Compatibility revision 6 replaces revision 5 without +changing dependency versions or the graph. Source/package checks pass; new +installed-Store and save-owner regressions have been added, including personal +and project stores with 100 deterministic barriers each. Local Chrome tests +have not run because the local shared dependency layout is incomplete. Official +tests, publication and packaged native acceptance remain pending. Historical +HOLDs remain HOLD; the full matrix is INCOMPLETE. See the +[release note](docs/releases/web-console-1.6.172.md). + Published `1.6.171` repairs a shared Store ordering defect: a delayed initial create response could overwrite a newer subscribe model and leave a successfully created local Volume stuck in its initial state. Only ID-less create POST/201 diff --git a/app/components/edit-apikey/component.js b/app/components/edit-apikey/component.js index 1ae75cebf9..2f6c63730f 100644 --- a/app/components/edit-apikey/component.js +++ b/app/components/edit-apikey/component.js @@ -8,6 +8,7 @@ export default ModalBase.extend(NewOrEdit, { model: null, clone: null, justCreated: false, + createOnlyDelivery: true, didReceiveAttrs() { this.set('clone', this.get('originalModel').clone()); @@ -52,7 +53,7 @@ export default ModalBase.extend(NewOrEdit, { { this.setProperties({ justCreated: true, - clone: neu.clone() + clone: this.cloneForCreateDelivery(neu) }); } }, diff --git a/app/mixins/new-or-edit.js b/app/mixins/new-or-edit.js index a219264423..f76214fd15 100644 --- a/app/mixins/new-or-edit.js +++ b/app/mixins/new-or-edit.js @@ -3,6 +3,7 @@ import { alias } from '@ember/object/computed'; import { service } from '@ember/service'; import Mixin from '@ember/object/mixin'; import Resource from 'ember-api-store/models/resource'; +import { bindCreateOnlyDelivery, cloneCreateOnlyDelivery, takeCreateOnlyDelivery } from 'ember-api-store/utils/create-only-delivery'; import Errors from 'ui/utils/errors'; export default Mixin.create({ @@ -11,6 +12,7 @@ export default Mixin.create({ errors: null, saving: false, editing: true, + createOnlyDelivery: false, primaryResource: alias('model'), originalPrimaryResource: alias('originalModel'), @@ -106,6 +108,14 @@ export default Mixin.create({ let finalizerError = null; if ( this._saveOwner === owner ) { + if ( this._createOnlyRequest && this._createOnlyRequest.owner === owner ) { + takeCreateOnlyDelivery(this._createOnlyRequest.options); + this._createOnlyRequest = null; + } + if ( this._createOnlyDelivery && this._createOnlyDelivery.owner === owner ) { + this._createOnlyDelivery.data.fields = null; + this._createOnlyDelivery = null; + } this._saveOwner = null; // A hook that turned saving on and then threw still owns that // state, but a submission which found a pre-existing saving=true @@ -199,11 +209,36 @@ export default Mixin.create({ }, doSave: function(opt) { + const owner = this._saveOwner; + if ( owner && this.get('createOnlyDelivery') ) { + opt = opt || {}; + Object.defineProperty(this, '_createOnlyRequest', { + value: { owner, options: opt }, writable: true, configurable: true, + }); + bindCreateOnlyDelivery(opt, (data) => { + if ( this._saveOwner === owner && !this.isDestroyed && !this.isDestroying ) { + Object.defineProperty(this, '_createOnlyDelivery', { + value: { owner, data }, writable: true, configurable: true, + }); + } else { + data.fields = null; + } + }); + } return this.get('primaryResource').save(opt).then((newData) => { return this.mergeResult(newData); }); }, + cloneForCreateDelivery(resource) { + const pending = this._createOnlyDelivery; + if ( !pending || pending.owner !== this._saveOwner ) { + return resource.clone(); + } + this._createOnlyDelivery = null; + return cloneCreateOnlyDelivery(resource, pending.data); + }, + mergeResult: function(newData) { var original = this.get('originalPrimaryResource'); if ( original ) diff --git a/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json b/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json index c5d74e292d..1d8631e649 100644 --- a/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json +++ b/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json @@ -1,12 +1,12 @@ { "name": "@pasturestack/web-console", - "version": "1.6.171", + "version": "1.6.172", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.171", + "version": "1.6.172", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" @@ -33,7 +33,7 @@ "core-js": "file:vendor/core-js-compat/core-js-2.6.13-rc16.0.tgz", "d3": "7.9.0", "dagre-d3-es": "7.0.14", - "ember-api-store": "file:vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.5.tgz", + "ember-api-store": "file:vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.6.tgz", "ember-auto-import": "2.13.1", "ember-basic-dropdown": "9.0.0", "ember-cli": "7.2.0", @@ -9051,8 +9051,8 @@ }, "node_modules/ember-api-store": { "version": "2.8.5", - "resolved": "file:vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.5.tgz", - "integrity": "sha512-m+IpOrSUqogl3EP8DqefpDuO/9leZ4Bycge7MLwqYASOz75V/J6ay1bFGaOWd2ckaohymODeOlkVzyVzmWLupw==", + "resolved": "file:vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.6.tgz", + "integrity": "sha512-ojkclvGZq8iObzwSkOBtZxkt9IwZ0GJvmi8CMYFocBwol6YQh9Q4D6g4t6/o/3mNDYmDkULPgdXBVt81xm5BqA==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/docs/releases/web-console-1.6.171.md b/docs/releases/web-console-1.6.171.md index c692d3cc85..ebd1d1d8ee 100644 --- a/docs/releases/web-console-1.6.171.md +++ b/docs/releases/web-console-1.6.171.md @@ -85,7 +85,6 @@ existing 2026-10-10 review boundary. Formal package checks found no affected build-package modules in the static artifact, without making a runtime not-affected VEX or zero-vulnerability claim. No dependency or security-policy change is introduced by this documentation update. - ## Upgrade and rollback Use the separately released Server patch that packages this exact component. diff --git a/docs/releases/web-console-1.6.172.md b/docs/releases/web-console-1.6.172.md new file mode 100644 index 0000000000..f64e5d5f32 --- /dev/null +++ b/docs/releases/web-console-1.6.172.md @@ -0,0 +1,65 @@ +# Web Console 1.6.172 + +Candidate first-delivery repair. Official tests, publication and packaged QA +are separate pending gates; historical failed receipts remain HOLD. + +## Root cause and contract + +Revision 5 correctly adopts a newer cached subscribe model instead of importing +an older POST/201 snapshot. For an API key, subscribe can already contain +`secretValue: null`; discarding the whole 201 also loses its only delivery of the +new secret. The API-key editor therefore cannot show its expected detached +first-delivery clone. Requiring canonical Store secrets to survive later +redacted subscribe updates is neither the fix nor the acceptance contract. + +Engine 333 source `0d94f7d879d314235e582a7f4062914a27b82709` maps auth-overlay +permission `o` to `FieldImpl.readOnCreateOnly` in `AuthOverlayPostProcessor`. +`Field.isReadOnCreateOnly` and its JavaBean implementation export the actual +Schema resource-field property `readOnCreateOnly`. This repair uses that exact +property; it does not invent a schema flag or merge all create-response fields. + +## Minimal change + +An ID-less create request captures only Schema-marked field names in its +nonenumerable internal identity metadata. An opt-in request-private Symbol +callback transports only those successful POST/201 values. API-key editing is +the sole NewOrEdit opt-in. Values are withheld from the canonical import and +consumed once into the editor's detached clone, whose visible/copy value remains +independent of later subscribe redaction. Normal cached state and nested models +are not re-imported from the older response. + +Delivery requires the same Store, generation, API base, exact generated ID, +concrete type and account binding. The existing save owner clears only its own +pending callback and delivery, including failed hooks and synchronous completion +exceptions. Duplicate submissions neither resend create nor clear the owner's +lock or values. Existing hook arguments/results, non-opted-in consumers, backend +permissions, API payloads and request counts remain unchanged. + +API-store compatibility revision 6 is a new archive. Earlier archives and +upstream license text are retained; dependency versions and graph are unchanged. +The source/package checker accepts Windows license line endings while requiring +the unchanged upstream content and exact source/archive equality. + +## Verification boundary + +The ten prior installed-Store ordering regressions remain. Added cases cover +actual API-key doneSaving delivery, redacted subscribe before 201, later +redaction, personal/project Stores (100 deterministic deferred HTTP barriers +each, without sleeps), uncached one-shot delivery, private metadata, store/ +generation/base/type/account mismatch, and synchronous delivery exceptions. +NewOrEdit tests cover delivery cleanup across success, request rejection, +synchronous doneSaving/completion exceptions and rejected duplicate submissions; +ordinary consumers keep their prior options and return value. + +Source/package checks pass. The local Chrome suite has not started because the +local junction-based dependency layout is incomplete; it is not reported as a +test PASS. Exact-source official tests, immutable publication and packaged native +first-delivery acceptance remain pending. Historical HOLDs and the complete +permission/resource/locale matrix are not promoted. + +## Upgrade and rollback + +Use only a separately published Server package containing this exact component. +Retain existing settings, persistent volumes and previous immutable artifacts. +No database migration or backend change is required. This candidate does not +authorize deployment, live retries or a change to authentication settings. diff --git a/package-lock.json b/package-lock.json index c5d74e292d..1d8631e649 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pasturestack/web-console", - "version": "1.6.171", + "version": "1.6.172", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.171", + "version": "1.6.172", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" @@ -33,7 +33,7 @@ "core-js": "file:vendor/core-js-compat/core-js-2.6.13-rc16.0.tgz", "d3": "7.9.0", "dagre-d3-es": "7.0.14", - "ember-api-store": "file:vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.5.tgz", + "ember-api-store": "file:vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.6.tgz", "ember-auto-import": "2.13.1", "ember-basic-dropdown": "9.0.0", "ember-cli": "7.2.0", @@ -9051,8 +9051,8 @@ }, "node_modules/ember-api-store": { "version": "2.8.5", - "resolved": "file:vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.5.tgz", - "integrity": "sha512-m+IpOrSUqogl3EP8DqefpDuO/9leZ4Bycge7MLwqYASOz75V/J6ay1bFGaOWd2ckaohymODeOlkVzyVzmWLupw==", + "resolved": "file:vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.6.tgz", + "integrity": "sha512-ojkclvGZq8iObzwSkOBtZxkt9IwZ0GJvmi8CMYFocBwol6YQh9Q4D6g4t6/o/3mNDYmDkULPgdXBVt81xm5BqA==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 28758ebccb..22851ccf29 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pasturestack/web-console", - "version": "1.6.171", + "version": "1.6.172", "private": true, "description": "PastureStack browser console for the compatible control platform.", "repository": { @@ -76,7 +76,7 @@ "core-js": "file:vendor/core-js-compat/core-js-2.6.13-rc16.0.tgz", "d3": "7.9.0", "dagre-d3-es": "7.0.14", - "ember-api-store": "file:vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.5.tgz", + "ember-api-store": "file:vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.6.tgz", "ember-auto-import": "2.13.1", "ember-basic-dropdown": "9.0.0", "ember-cli": "7.2.0", diff --git a/scripts/check-modernization-blockers b/scripts/check-modernization-blockers index d78bc35a33..40caa864f2 100755 --- a/scripts/check-modernization-blockers +++ b/scripts/check-modernization-blockers @@ -41,8 +41,8 @@ with open('package.json', encoding='utf-8') as f: print(json.load(f).get('version', '')) PY ) -if [[ "$version" != "1.6.171" ]]; then - echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.171" +if [[ "$version" != "1.6.172" ]]; then + echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.172" failures=$((failures + 1)) fi diff --git a/scripts/check-ui-console-workspace b/scripts/check-ui-console-workspace index 60fc4ade01..35d05d92c9 100755 --- a/scripts/check-ui-console-workspace +++ b/scripts/check-ui-console-workspace @@ -141,4 +141,4 @@ if [[ -n ${PASTURESTACK_PRIVATE_MARKER:-} ]] && grep -RInF -- "$PASTURESTACK_PRI fi printf 'UI_CONSOLE_WORKSPACE_OK version=%s persistence=%s cross_tab=%s\n' \ - 1.6.171 browser-session broker-broadcast + 1.6.172 browser-session broker-broadcast diff --git a/scripts/check-ui-critical-high-dependencies b/scripts/check-ui-critical-high-dependencies index 51ff79c629..0b2c02ef9b 100755 --- a/scripts/check-ui-critical-high-dependencies +++ b/scripts/check-ui-critical-high-dependencies @@ -57,7 +57,7 @@ for gate in ("node ./scripts/test-ui-npm-audit.js", "node ./scripts/check-ui-npm for evidence in ("scripts/check-ui-npm-audit.js", "scripts/test-ui-npm-audit.js", "docs/security/npm-vendor-pending.json"): if not Path(evidence).is_file(): fail(f"reviewed live audit evidence is missing: {evidence}") -api_store_compat_spec = "file:vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.5.tgz" +api_store_compat_spec = "file:vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.6.tgz" lock_bytes = lock_path.read_bytes() baseline_bytes = baseline_path.read_bytes() if lock_bytes != baseline_bytes: @@ -70,7 +70,7 @@ if lock_bytes != baseline_bytes: lock = json.loads(lock_bytes) packages = lock.get("packages", {}) root = packages.get("", {}) -if package.get("version") != "1.6.171": +if package.get("version") != "1.6.172": fail(f"unexpected Web Console version: {package.get('version')}") if root.get("version") != package.get("version"): fail(f"lock root version differs: {root.get('version')}") diff --git a/scripts/check-ui-ember-api-store-fetch-upgrade b/scripts/check-ui-ember-api-store-fetch-upgrade index de21614f27..84d18eaa17 100755 --- a/scripts/check-ui-ember-api-store-fetch-upgrade +++ b/scripts/check-ui-ember-api-store-fetch-upgrade @@ -13,7 +13,7 @@ package = json.loads(package_path.read_text(encoding="utf-8")) lock = json.loads(lock_path.read_text(encoding="utf-8")) packages = lock.get("packages", {}) compat_spec = "file:vendor/ember-fetch-compat/ember-fetch-5.1.3-pasturestack.6.tgz" -api_store_compat_spec = "file:vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.5.tgz" +api_store_compat_spec = "file:vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.6.tgz" def fail(message): @@ -110,24 +110,24 @@ with tarfile.open(archive_path, "r:gz") as archive: fail("ember-fetch compatibility source and install archive differ") api_store_compat_dir = repo / "vendor/ember-api-store-compat" -api_store_archive = api_store_compat_dir / "ember-api-store-2.8.5-pasturestack.5.tgz" +api_store_archive = api_store_compat_dir / "ember-api-store-2.8.5-pasturestack.6.tgz" api_store_package_path = api_store_compat_dir / "package.json" api_store_license_path = api_store_compat_dir / "LICENSE" api_store_upstream_path = api_store_compat_dir / "UPSTREAM.md" for required in [api_store_archive, api_store_package_path, api_store_license_path, api_store_upstream_path]: if not required.is_file(): fail(f"reviewed ember-api-store compatibility file missing: {required.relative_to(repo)}") -if hashlib.sha256(api_store_archive.read_bytes()).hexdigest() != "90da9ebdc36a8069629086d011e799691ace8f88c13d9c9df1333c77015a2ab8": +if hashlib.sha256(api_store_archive.read_bytes()).hexdigest() != "58079a9d1cc9539d89999f9fb3ac9f89464a1d79f45bb1c8ada18b8eda545bb6": fail("reviewed ember-api-store compatibility archive hash changed") -if hashlib.sha256(api_store_license_path.read_bytes()).hexdigest() != "0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594": +if hashlib.sha256(api_store_license_path.read_bytes().replace(b"\r\n", b"\n")).hexdigest() != "0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594": fail("ember-api-store upstream Apache-2.0 license changed") api_store_package = json.loads(api_store_package_path.read_text(encoding="utf-8")) if api_store_package.get("dependencies") != expected_api_store_deps: fail("ember-api-store compatibility source metadata changed") -if api_store_package.get("pasturestackCompatibility", {}).get("revision") != 5: +if api_store_package.get("pasturestackCompatibility", {}).get("revision") != 6: fail("ember-api-store compatibility revision marker is missing") with tarfile.open(api_store_archive, "r:gz") as archive: - for relative in ["package.json", "LICENSE", "UPSTREAM.md", "addon/services/store.js", "addon/mixins/type.js"]: + for relative in ["package.json", "LICENSE", "UPSTREAM.md", "addon/services/store.js", "addon/mixins/type.js", "addon/utils/create-only-delivery.js"]: archived = archive.extractfile(f"package/{relative}") source = api_store_compat_dir / relative if archived is None or archived.read() != source.read_bytes(): @@ -154,7 +154,7 @@ action_dispatch = type_runtime.split(" doAction: function(name, data, opt) {", if "delete opt.createIdentity;" not in action_dispatch: fail("action POST must clear any reused create identity") request_success = api_store_runtime.split(" _requestSuccess(xhr,opt) {", 1)[1].split(" _requestFailed(xhr,opt) {", 1)[0] -for marker in ["delete opt.createIdentity;", "if ( opt.method === 'POST' )", "opt.createIdentity = {", "generation: get(store, 'generation')", "baseUrl: get(store, 'baseUrl')"]: +for marker in ["delete opt.createIdentity;", "if ( opt.method === 'POST' )", "Object.defineProperty(opt, 'createIdentity'", "generation: get(store, 'generation')", "baseUrl: get(store, 'baseUrl')", "fields[key].readOnCreateOnly === true"]: if marker not in create_save: fail(f"create-only save identity marker missing: {marker}") for marker in ["xhr.status === 201 && opt.method === 'POST' && creation", "creation.generation === get(this, 'generation')", "creation.baseUrl === get(this, 'baseUrl')", "cached.get('id') === xhr.body.id", "get(cached, 'store') === this && this.hasRecord(cached)", "response = response || this._typeify(xhr.body);"]: @@ -172,6 +172,10 @@ for marker in [ "204 and errors keep their HTTP semantics without importing a model", "reusing save options cannot carry a create marker into an existing record save", "action POST cannot reuse an old create marker even when the action returns 201", + "apiKey first delivery survives redacted subscribe before 201 and later redaction in personal and project stores, 100 deterministic barriers each", + "uncached API-key create-only delivery is one-shot and schema-bound, with private request metadata", + "create-only delivery rejects changed store, generation, base, concrete type and owner without importing its secret", + "a synchronous first-delivery callback exception is consumed once and cannot replay create", ]: if marker not in create_order_tests: fail(f"API-store create response order regression missing: {marker}") @@ -454,7 +458,7 @@ for marker in [ deprecated = [path for path, item in packages.items() if item.get("deprecated")] print( "ui-ember-api-store-fetch-upgrade-ok " - f"version=2.8.5 api_store_compat_revision=5 ember-fetch=5.1.3 fetch_compat_revision=6 initializer_compat_revision=2 reference_compat_revision=2 " + f"version=2.8.5 api_store_compat_revision=6 ember-fetch=5.1.3 fetch_compat_revision=6 initializer_compat_revision=2 reference_compat_revision=2 " f"ember6_template_compat_revision=1 terminal_reconnect_revision=2 " f"deprecated_count={len(deprecated)} package_count={len(packages)}" ) diff --git a/scripts/node24-lock-smoke.js b/scripts/node24-lock-smoke.js index 9f85ad89c4..5b3f848e14 100644 --- a/scripts/node24-lock-smoke.js +++ b/scripts/node24-lock-smoke.js @@ -701,7 +701,7 @@ function expectEmberApiStoreFetchUpgrade() { if (JSON.stringify(apiStoreInfo.dependencies) !== JSON.stringify(expectedApiStoreDependencies)) { fail(`ember-api-store reviewed dependency boundary changed: ${JSON.stringify(apiStoreInfo.dependencies)}`); } - if (!apiStoreInfo.pasturestackCompatibility || apiStoreInfo.pasturestackCompatibility.revision !== 5) { + if (!apiStoreInfo.pasturestackCompatibility || apiStoreInfo.pasturestackCompatibility.revision !== 6) { fail("ember-api-store compatibility revision is missing"); } if (!emberFetchInfo.pasturestackCompatibility || emberFetchInfo.pasturestackCompatibility.revision !== 6) { @@ -725,6 +725,7 @@ function expectEmberApiStoreFetchUpgrade() { "addon/utils/fetch.js", "addon/utils/denormalize.js", "addon/utils/normalize.js", + "addon/utils/create-only-delivery.js", ]) { if (!fs.existsSync(path.join(apiStoreDir, filePath))) { fail(`ember-api-store required API file missing: ${filePath}`); @@ -738,15 +739,15 @@ function expectEmberApiStoreFetchUpgrade() { fail("ember-api-store deferred request initialization fix is missing"); } - expectVendoredFileSha256("vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.5.tgz", - "90da9ebdc36a8069629086d011e799691ace8f88c13d9c9df1333c77015a2ab8"); + expectVendoredFileSha256("vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.6.tgz", + "58079a9d1cc9539d89999f9fb3ac9f89464a1d79f45bb1c8ada18b8eda545bb6"); const typeMixin = fs.readFileSync(path.join(apiStoreDir, "addon/mixins/type.js"), "utf8"); const actionDispatch = typeMixin.split(" doAction: function(name, data, opt) {")[1].split(" save: function(opt) {")[0]; if (!actionDispatch.includes("delete opt.createIdentity;")) { fail("ember-api-store action POST must clear any reused create identity"); } - for (const marker of ["delete opt.createIdentity;", "if ( opt.method === 'POST' )", "opt.createIdentity = {", - "generation: get(store, 'generation')", "baseUrl: get(store, 'baseUrl')"]) { + for (const marker of ["delete opt.createIdentity;", "if ( opt.method === 'POST' )", "Object.defineProperty(opt, 'createIdentity'", + "generation: get(store, 'generation')", "baseUrl: get(store, 'baseUrl')", "fields[key].readOnCreateOnly === true"]) { if (!typeMixin.includes(marker)) { fail(`ember-api-store create-only save identity marker missing: ${marker}`); } @@ -831,7 +832,7 @@ function expectEmberApiStoreFetchUpgrade() { fail("ember-fetch native production wrapper smoke failed"); } - console.log("ember-api-store-fetch-upgrade-smoke-ok version=2.8.5 api_store_compat_revision=5 ember-fetch=5.1.3 fetch_compat_revision=6 native_fetch=ok legacy_build_graph=absent"); + console.log("ember-api-store-fetch-upgrade-smoke-ok version=2.8.5 api_store_compat_revision=6 ember-fetch=5.1.3 fetch_compat_revision=6 native_fetch=ok legacy_build_graph=absent"); } function expectBrowserGlobalBundle(file, globalName, expectedVersion) { diff --git a/tests/unit/mixins/new-or-edit-test.js b/tests/unit/mixins/new-or-edit-test.js index ed8b875c70..c37bd4936b 100644 --- a/tests/unit/mixins/new-or-edit-test.js +++ b/tests/unit/mixins/new-or-edit-test.js @@ -4,6 +4,7 @@ import { run } from '@ember/runloop'; import { defer, reject, resolve } from 'rsvp'; import { module, test } from 'qunit'; import NewOrEdit from 'ui/mixins/new-or-edit'; +import { takeCreateOnlyDelivery } from 'ember-api-store/utils/create-only-delivery'; module('Unit | Mixin | new or edit'); @@ -82,6 +83,79 @@ function save(subject, callback) { return subject.get('actions').save.call(subject, callback); } +test('opt-in delivery belongs to one save owner and is cleared on success, rejection and synchronous callback failures', async function(assert) { + for ( const mode of ['success', 'request-reject', 'doneSaving-throw', 'completion-throw'] ) { + const pending = defer(); + const failure = new Error(mode); + let options, callbackCalls = 0; + const subject = subjectWith({ + createOnlyDelivery: true, + doneSaving(value) { + assert.strictEqual(value, 'saved', 'existing hook argument is unchanged'); + if ( mode === 'doneSaving-throw' ) { throw failure; } + return value; + }, + }); + subject.get('model').save = value => { options = value; return pending.promise; }; + const operation = save(subject, () => { + callbackCalls++; + if ( mode === 'completion-throw' ) { throw failure; } + }); + for ( let turn = 0; !options && turn < 20; turn++ ) { await resolve(); } + assert.ok(options, 'base doSave binds private delivery options'); + const data = {fields: {secretValue: 'SECRET-TEST'}}; + takeCreateOnlyDelivery(options)(data); + assert.notOk(Object.keys(subject).includes('_createOnlyDelivery'), 'delivery metadata is nonenumerable'); + assert.notOk(Object.keys(subject).includes('_createOnlyRequest'), 'request metadata is nonenumerable'); + const owner = subject._saveOwner; + assert.deepEqual(await save(subject), {saved: false, reason: 'busy'}); + assert.strictEqual(subject._saveOwner, owner, 'duplicate cannot clear the owner'); + assert.strictEqual(subject._createOnlyDelivery.data, data, 'duplicate cannot consume the owner delivery'); + if ( mode === 'request-reject' ) { pending.reject(failure); } else { pending.resolve('saved'); } + try { + const result = await operation; + if ( mode === 'completion-throw' ) { assert.ok(false, 'callback failure must reject'); } + else if ( mode === 'success' ) { assert.strictEqual(result, 'saved', 'existing result is unchanged'); } + else { assert.strictEqual(result.error, failure); } + } catch (error) { + assert.strictEqual(error, failure); + assert.strictEqual(mode, 'completion-throw'); + } + assert.strictEqual(callbackCalls, 1); + assert.strictEqual(data.fields, null, `${mode} clears unconsumed one-time values`); + assert.strictEqual(takeCreateOnlyDelivery(options), null); + assert.strictEqual(subject._createOnlyRequest, null); + assert.strictEqual(subject._createOnlyDelivery, null); + assert.strictEqual(subject._saveOwner, null); + assert.strictEqual(subject.get('saving'), false); + run(() => subject.destroy()); + } +}); + +test('synchronous persistence failure clears private callback, while ordinary consumers keep their options and result', async function(assert) { + let options; + const subject = subjectWith({createOnlyDelivery: true}); + const failure = new Error('synchronous save failed'); + subject.get('model').save = value => { options = value; throw failure; }; + const outcome = await save(subject); + assert.strictEqual(outcome.error, failure); + assert.strictEqual(takeCreateOnlyDelivery(options), null); + assert.strictEqual(subject._createOnlyRequest, null); + assert.strictEqual(subject._saveOwner, null); + assert.strictEqual(subject.get('saving'), false); + run(() => subject.destroy()); + + const ordinary = subjectWith(); + ordinary.get('model').save = value => { + assert.strictEqual(value, undefined, 'non-opted-in consumers keep their previous arguments'); + return resolve('ordinary-saved'); + }; + assert.strictEqual(await save(ordinary), 'ordinary-saved'); + assert.strictEqual(ordinary._createOnlyRequest, undefined); + assert.strictEqual(ordinary._createOnlyDelivery, undefined); + run(() => ordinary.destroy()); +}); + test('validation cancellation returns an awaitable outcome and completes once', function(assert) { let callbacks = []; let saves = 0; diff --git a/tests/unit/vendor/api-store-create-order-test.js b/tests/unit/vendor/api-store-create-order-test.js index ec45c59669..c66b5e6a7b 100644 --- a/tests/unit/vendor/api-store-create-order-test.js +++ b/tests/unit/vendor/api-store-create-order-test.js @@ -6,14 +6,23 @@ import Store from 'ember-api-store/services/store'; import Resource from 'ember-api-store/models/resource'; import Schema from 'ember-api-store/models/schema'; import Collection from 'ember-api-store/models/collection'; +import { bindCreateOnlyDelivery, cloneCreateOnlyDelivery, takeCreateOnlyDelivery } from 'ember-api-store/utils/create-only-delivery'; +import EditApiKey from 'ui/components/edit-apikey/component'; +import EmberObject from '@ember/object'; +import Service from '@ember/service'; +import inertRenderer from '../../helpers/inert-renderer'; +import { createOwned, destroyOwned } from '../../helpers/owned-subject'; // Real installed compatibility package and Type.save. Only the HTTP boundary // is deferred: subscribe import must complete before the original 201 arrives. -function fixture(project = '1a-test') { +function fixture(project = '1a-test', baseUrl = `/v2-beta/projects/${project}`) { const objects = new Set(); const requests = []; - const store = Store.create({ baseUrl: `/v2-beta/projects/${project}` }); + const intl = Service.create({exists() { return false; }, t(key) { return key; }}); + objects.add(intl); + const store = Store.create({ baseUrl }); setOwner(store, { lookup(name) { + if ( name === 'service:intl' ) { return intl; } if ( name === 'service:fastboot' ) { return { isFastBoot: false }; } const Factory = name === 'model:schema' ? Schema : name === 'model:collection' ? Collection : Resource; @@ -24,16 +33,31 @@ function fixture(project = '1a-test') { const createRecord = store.createRecord.bind(store); store.createRecord = (...args) => { const object = createRecord(...args); + // The real Resource validator reads model.intl, not component.intl. + // Service injection is not an enumerable resource/API payload field. + Object.defineProperty(object, 'intl', {value: intl, configurable: true}); objects.add(object); return object; }; const response = defer(); - store.rawRequest = (options) => { requests.push(options); return response.promise; }; + const requestEntered = defer(); + store.rawRequest = (options) => { + requests.push(options); + requestEntered.resolve(options); + return response.promise; + }; store._bulkAdd('schema', ['volume', 'loadBalancerService', 'service'].map(id => ({ type: 'schema', id, resourceFields: {}, collectionMethods: ['GET', 'POST'], links: { collection: `${store.baseUrl}/${id}s` }, }))); - return { store, requests, response, + store._bulkAdd('schema', [{type: 'schema', id: 'apiKey', + collectionMethods: ['GET', 'POST'], resourceFields: { + name: {type: 'string', create: true}, + publicValue: {type: 'string', create: false}, + secretValue: {type: 'password', create: false, update: false, readOnCreateOnly: true}, + nested: {type: 'service'}, + }, links: {collection: `${store.baseUrl}/apikeys`}}]); + return { store, requests, response, requestEntered, intl, destroy() { store.all('schema').forEach(object => objects.add(object)); run(() => { @@ -52,6 +76,162 @@ const current = (type = 'volume', id = 'Opaque-ID') => ({ }); module('Unit | Vendor | API store create response order', function() { + test('apiKey first delivery survives redacted subscribe before 201 and later redaction in personal and project stores, 100 deterministic barriers each', async function(assert) { + for ( const [accountId, baseUrl] of [['1a-owner', '/v2-beta'], ['1a-project', '/v2-beta/projects/1a-project']] ) { + for ( let index = 0; index < 100; index++ ) { + const f = fixture(accountId, baseUrl); + let subject; + try { + const original = f.store.createRecord({type: 'apiKey', name: 'created', accountId}); + const draft = original.clone(); + subject = createOwned(EditApiKey, { + renderer: inertRenderer(), + intl: f.intl, + modalService: EmberObject.create({modalOpts: original}), + model: draft, + clone: original.clone(), + didSave(resource) { + assert.strictEqual(imports, 0, '201 adoption performs no stale mangleIn or nested import'); + return resource; + }, + }, 'component'); + const savingModel = subject.get('primaryResource'); + assert.strictEqual(savingModel, subject.get('model'), 'actual editor saves its model'); + assert.strictEqual(savingModel.get('intl'), f.intl, 'actual Resource receives the shared intl service'); + assert.deepEqual(savingModel.validationErrors(), [], 'real model validation accepts the fixture without bypassing willSave'); + assert.notOk(Object.hasOwn(savingModel.serialize(), 'intl'), 'service is not serialized into create payload'); + assert.strictEqual(savingModel.get('accountId'), accountId); + assert.notOk(savingModel.get('id'), 'actual editor starts with an ID-less draft'); + const completion = []; + const saving = run(() => subject.get('actions').save.call(subject, success => completion.push(success))); + // Wait for real transport entry. A cancelled/failed lifecycle wins + // the race instead of hanging or assuming N microtask turns suffice. + const boundary = await Promise.race([ + f.requestEntered.promise.then(options => ({options})), + saving.then(outcome => ({earlyOutcome: outcome}), error => ({earlyError: error})), + ]); + const earlyError = boundary.earlyError || (boundary.earlyOutcome && boundary.earlyOutcome.error); + const diagnosis = {reason: boundary.earlyOutcome && boundary.earlyOutcome.reason, + saved: boundary.earlyOutcome && boundary.earlyOutcome.saved, + error: earlyError && earlyError.message, errors: subject.get('errors')}; + assert.ok(boundary.options, `actual request entered, otherwise early lifecycle: ${JSON.stringify(diagnosis)}`); + assert.strictEqual(f.requests.length, 1, 'the actual save reaches its deferred HTTP boundary'); + if ( !boundary.options ) { return; } + const nested = run(() => f.store._typeify({...current('service', 'Nested-ID'), state: 'active'})); + run(() => f.store._typeify({type: 'apiKey', id: 'Key-ID', accountId, + name: 'created', state: 'active', publicValue: 'PUBLIC-TEST', secretValue: null, nested})); + let imports = 0; + const createRecord = f.store.createRecord; + f.store.createRecord = (...args) => { imports++; return createRecord(...args); }; + const body = {type: 'apiKey', id: 'Key-ID', accountId, name: 'created', + state: 'registering', publicValue: 'PUBLIC-TEST', secretValue: 'SECRET-TEST', + nested: {...initial('service', 'Nested-ID'), state: 'creating'}}; + const xhr = {status: 201, body}; + run(() => f.response.resolve(xhr)); + await saving; + const canonical = f.store.getById('apiKey', 'Key-ID'); + const clone = subject.get('clone'); + assert.strictEqual(canonical, savingModel, 'actual editor draft canonical save identity is retained'); + assert.strictEqual(canonical.get('state'), 'active', 'newer cached state wins'); + assert.strictEqual(nested.get('state'), 'active', 'stale nested 201 is not imported'); + assert.strictEqual(clone.get('secretValue'), 'SECRET-TEST', 'actual API-key doneSaving receives one-time secret'); + assert.strictEqual(clone.get('publicValue'), 'PUBLIC-TEST'); + assert.strictEqual(canonical.get('secretValue'), null, 'canonical store never needs to retain secret'); + assert.notOk(JSON.stringify(canonical.serialize()).includes('SECRET-TEST')); + assert.notOk(Object.hasOwn(f.requests[0].data, 'createIdentity')); + assert.notOk(Object.keys(f.requests[0]).includes('createIdentity'), 'create marker is nonenumerable'); + assert.notOk(JSON.stringify(f.requests[0]).includes('SECRET-TEST'), 'request metadata contains no secret'); + assert.deepEqual(completion, [true]); + assert.strictEqual(subject._createOnlyDelivery, null); + assert.strictEqual(subject._createOnlyRequest, null); + assert.strictEqual(subject._saveOwner, null); + assert.strictEqual(subject.get('saving'), false); + assert.notOk(xhr.body, 'raw 201 body is not retained'); + run(() => f.store._typeify({type: 'apiKey', id: 'Key-ID', accountId, + name: 'created', state: 'active', publicValue: 'PUBLIC-TEST', secretValue: null})); + assert.strictEqual(canonical.get('secretValue'), null); + assert.strictEqual(clone.get('secretValue'), 'SECRET-TEST', 'later WS cannot erase detached visible delivery'); + assert.strictEqual(f.requests.length, 1, 'no replay or extra request'); + } finally { + if ( subject ) { destroyOwned(subject); } + f.destroy(); + } + } + } + }); + + test('uncached API-key create-only delivery is one-shot and schema-bound, with private request metadata', async function(assert) { + const f = fixture(); + try { + let data, calls = 0; + const options = {}; + bindCreateOnlyDelivery(options, value => { data = value; calls++; }); + const draft = f.store.createRecord({type: 'apiKey', name: 'created'}); + const saving = run(() => draft.save(options)); + run(() => f.response.resolve({status: 201, body: {type: 'apiKey', id: 'Key-ID', + state: 'requested', name: 'created', secretValue: 'SECRET-TEST', publicValue: 'PUBLIC-TEST'}})); + assert.strictEqual(await saving, draft); + assert.strictEqual(calls, 1); + assert.deepEqual(data.fields, {secretValue: 'SECRET-TEST'}, 'only exact readOnCreateOnly=true fields are delivered'); + assert.strictEqual(draft.get('secretValue'), null); + const clone = cloneCreateOnlyDelivery(draft, data); + assert.strictEqual(clone.get('secretValue'), 'SECRET-TEST'); + assert.strictEqual(data.fields, null, 'delivery is consumed'); + assert.throws(() => cloneCreateOnlyDelivery(draft, data), /no longer belongs/); + assert.strictEqual(takeCreateOnlyDelivery(options), null); + } finally { f.destroy(); } + }); + + test('create-only delivery rejects changed store, generation, base, concrete type and owner without importing its secret', async function(assert) { + for ( const change of ['generation', 'base', 'type', 'owner'] ) { + const f = fixture(); + try { + let calls = 0; + const options = {}; + bindCreateOnlyDelivery(options, () => calls++); + const draft = f.store.createRecord({type: 'apiKey', accountId: '1a-test'}); + const saving = run(() => draft.save(options)); + if ( change === 'generation' ) { run(() => f.store.reset()); } + if ( change === 'base' ) { f.store.set('baseUrl', '/v2-beta/projects/other'); } + if ( change === 'owner' ) { run(() => f.store._typeify({type: 'apiKey', id: 'Key-ID', accountId: '1a-other', state: 'active', secretValue: null})); } + run(() => f.response.resolve({status: 201, body: {type: change === 'type' ? 'volume' : 'apiKey', id: 'Key-ID', accountId: '1a-test', secretValue: 'SECRET-TEST'}})); + await saving; + assert.strictEqual(calls, 0, `${change} cannot receive first delivery`); + assert.notStrictEqual(draft.get('secretValue'), 'SECRET-TEST'); + } finally { f.destroy(); } + } + const f = fixture(), other = fixture(); + try { + const resource = other.store._typeify({type: 'apiKey', id: 'Key-ID'}); + assert.throws(() => cloneCreateOnlyDelivery(resource, {id: 'Key-ID', type: 'apikey', + store: f.store, generation: f.store.generation, baseUrl: f.store.baseUrl, fields: {secretValue: 'SECRET-TEST'}}), /no longer belongs/); + } finally { f.destroy(); other.destroy(); } + }); + + test('a synchronous first-delivery callback exception is consumed once and cannot replay create', async function(assert) { + const f = fixture(); + try { + let calls = 0, delivered; + const options = {}; + const failure = new Error('delivery callback failed'); + bindCreateOnlyDelivery(options, value => { delivered = value; calls++; throw failure; }); + const draft = f.store.createRecord({type: 'apiKey'}); + const saving = run(() => draft.save(options)); + // Attach before fulfilling HTTP: RSVP must not report an unhandled + // rejection while the native async test has not resumed yet. + const handled = saving.then(value => ({value}), error => ({error})); + run(() => f.response.resolve({status: 201, body: {type: 'apiKey', id: 'Key-ID', secretValue: 'SECRET-TEST'}})); + const outcome = await handled; + assert.ok(outcome.error, 'callback failure rejects create completion'); + assert.strictEqual(outcome.error.get('message'), failure.message, 'existing API error message is retained'); + assert.strictEqual(outcome.error.xhr, failure, 'existing API error wrapper retains the exact original exception'); + assert.strictEqual(calls, 1); + assert.strictEqual(delivered.fields, null, 'synchronous callback failure clears one-time values'); + assert.strictEqual(takeCreateOnlyDelivery(options), null); + assert.strictEqual(f.requests.length, 1); + assert.strictEqual(f.store.getById('apiKey', 'Key-ID').get('secretValue'), null); + } finally { f.destroy(); } + }); test('delayed 201 cannot overwrite the newer subscribe model, repeated with deterministic barriers 100 times', async function(assert) { for ( let index = 0; index < 100; index++ ) { const f = fixture(); diff --git a/vendor/ember-api-store-compat/UPSTREAM.md b/vendor/ember-api-store-compat/UPSTREAM.md index 35113d99fb..d13e346145 100644 --- a/vendor/ember-api-store-compat/UPSTREAM.md +++ b/vendor/ember-api-store-compat/UPSTREAM.md @@ -38,3 +38,13 @@ body. Save completion, base-type aliases, HTTP metadata and errors retain their existing contracts. GET, PUT, actions, non-201 responses and uncached creates continue through the original import path. This does not order resource states or event timestamps, grant permissions, change API responses, or add requests. + +Compatibility revision 6 adds opt-in first delivery of Schema fields explicitly +marked `readOnCreateOnly`, exported by the Engine auth overlay's `o` permission. +The create request captures only their field names. A request-private, +nonenumerable callback transports the successful 201 values once to a detached +consumer clone, outside the canonical store and serialized request. This keeps +revision 5's newer subscribe state and nested-resource adoption intact, validates +the same store, generation, API base, generated ID, concrete type and owner, and +does not require a canonical resource to retain secrets after create. Existing +save hooks, non-opted-in consumers, errors, and request counts are unchanged. diff --git a/vendor/ember-api-store-compat/addon/mixins/type.js b/vendor/ember-api-store-compat/addon/mixins/type.js index e240df0752..c0ff4aa15a 100644 --- a/vendor/ember-api-store-compat/addon/mixins/type.js +++ b/vendor/ember-api-store-compat/addon/mixins/type.js @@ -164,11 +164,13 @@ var Type = Mixin.create(Serializable,{ // A generated ID may arrive over subscribe before its original 201. // Bind this create-only adoption to the store that started the request. if ( opt.method === 'POST' ) { - opt.createIdentity = { - type, - generation: get(store, 'generation'), - baseUrl: get(store, 'baseUrl'), - }; + const schema = store.getById('schema', type); + const fields = schema && get(schema, 'store') === store ? get(schema, 'resourceFields') || {} : {}; + Object.defineProperty(opt, 'createIdentity', { + configurable: true, + value: { type, generation: get(store, 'generation'), baseUrl: get(store, 'baseUrl'), + readOnCreateFields: Object.keys(fields).filter(key => fields[key].readOnCreateOnly === true) }, + }); } } diff --git a/vendor/ember-api-store-compat/addon/services/store.js b/vendor/ember-api-store-compat/addon/services/store.js index 8f3700eecd..024524ad65 100644 --- a/vendor/ember-api-store-compat/addon/services/store.js +++ b/vendor/ember-api-store-compat/addon/services/store.js @@ -11,6 +11,7 @@ import { reject, resolve, defer } from 'rsvp'; import Service, { service } from '@ember/service'; import { isArray } from '@ember/array'; import { parse as setCookieParser } from 'set-cookie-parser'; +import { takeCreateOnlyDelivery } from '../utils/create-only-delivery'; function getOwnerKey() { const x = {}; @@ -510,6 +511,7 @@ var Store = Service.extend({ _requestSuccess(xhr,opt) { opt.responseStatus = xhr.status; + const firstDelivery = takeCreateOnlyDelivery(opt); if ( xhr.status === 204 ) { return; @@ -517,7 +519,24 @@ var Store = Service.extend({ if ( xhr.body && typeof xhr.body === 'object' ) { let response; + let delivery; const creation = opt.createIdentity; + const createOnlyFields = {}; + // Keep one-time fields out of the canonical import even if the original + // store generation changed before the response arrived. A stale marker + // must neither deliver its values nor retain them in a different cache. + if ( firstDelivery && creation && xhr.status === 201 && opt.method === 'POST' ) { + // Field names were captured from this request's actual Schema, so a + // later reset cannot turn a one-time value into a canonical field. + (creation.readOnCreateFields || []).forEach((key) => { + if ( Object.hasOwn(xhr.body, key) ) { + if ( xhr.body[key] !== null && xhr.body[key] !== undefined ) { + createOnlyFields[key] = JSON.parse(JSON.stringify(xhr.body[key])); + } + xhr.body[key] = null; + } + }); + } // Only a new-record save can use this rule. Its 201 is the initial // snapshot; the same generated ID already in this store has arrived // through subscribe while that response was in flight. Do not import @@ -533,10 +552,26 @@ var Store = Service.extend({ get(cached, 'store') === this && this.hasRecord(cached) ) { response = cached; } + if ( Object.keys(createOnlyFields).length && + (!response || get(response, 'accountId') === xhr.body.accountId) ) { + // Engine's auth overlay "o" exports readOnCreateOnly. Transport + // only those schema-bound values; no stale state or nested imports. + delivery = { id: xhr.body.id, type: creation.type, store: this, + accountId: xhr.body.accountId, generation: creation.generation, + baseUrl: creation.baseUrl, fields: createOnlyFields }; + } } response = response || this._typeify(xhr.body); delete xhr.body; Object.defineProperty(response, 'xhr', {value: xhr, configurable: true}); + if ( delivery ) { + try { + firstDelivery(delivery); + } catch (error) { + delivery.fields = null; + throw error; + } + } // Depaginate if ( opt.depaginate && typeof response.depaginate === 'function' ) { @@ -554,6 +589,7 @@ var Store = Service.extend({ }, _requestFailed(xhr,opt) { + takeCreateOnlyDelivery(opt); var body; if ( xhr.err ) { diff --git a/vendor/ember-api-store-compat/addon/utils/create-only-delivery.js b/vendor/ember-api-store-compat/addon/utils/create-only-delivery.js new file mode 100644 index 0000000000..c13aa7a397 --- /dev/null +++ b/vendor/ember-api-store-compat/addon/utils/create-only-delivery.js @@ -0,0 +1,36 @@ +import { get } from '@ember/object'; +import { normalizeType } from './normalize'; + +// Request-local metadata: not a resource field, payload key, or store cache. +const callbackKey = Symbol('create-only first delivery'); + +export function bindCreateOnlyDelivery(options, callback) { + Object.defineProperty(options, callbackKey, { value: callback, configurable: true }); +} + +export function takeCreateOnlyDelivery(options) { + const callback = options[callbackKey]; + delete options[callbackKey]; + return typeof callback === 'function' ? callback : null; +} + +export function cloneCreateOnlyDelivery(resource, delivery) { + if ( !delivery ) { + return resource.clone(); + } + + const fields = delivery.fields; + delivery.fields = null; // Consume even when identity validation or cloning fails. + const store = get(resource, 'store'); + if ( !fields || store !== delivery.store || get(resource, 'id') !== delivery.id || + normalizeType(get(resource, 'type')) !== delivery.type || + get(resource, 'accountId') !== delivery.accountId || + get(store, 'generation') !== delivery.generation || + get(store, 'baseUrl') !== delivery.baseUrl ) { + throw new Error('Create-only delivery no longer belongs to this save'); + } + + const clone = resource.clone(); + clone.setProperties(fields); + return clone; +} diff --git a/vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.6.tgz b/vendor/ember-api-store-compat/ember-api-store-2.8.5-pasturestack.6.tgz new file mode 100644 index 0000000000000000000000000000000000000000..b4c9c262ee13cbdef5d5c2018ab821a7adeae65f GIT binary patch literal 24320 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" }, "pasturestackCompatibility": { - "revision": 5, + "revision": 6, "upstreamPackage": "ember-api-store", "upstreamVersion": "2.8.5", "upstreamIntegrity": "sha512-YvnBZfdNGG7hB25hecEENHObXNN+186Bbkd1wAIL7qtRXqboQsQxTU05xxP7axgW6TPYfUYMxZ+GgbHgD14g2A=="