Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/30147-protocol-receiver-forms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@objectstack/metadata-protocol': patch
---

refactor(metadata-protocol): nineteen more `protocol.ts` members hand their bodies to their facets

Clause-②: no
Metadata-protocol: none

- Nineteen members of `ObjectStackProtocolImplementation` that the facade gate still let keep a body (the overlay and runtime-create write gates, the code-only refusals, the registry hydration's view-expansion reconcile and four static type tables) now keep their signatures on the class and delegate in one line to their section's facet, which holds the body, moved unchanged. Four static tables moved to a new leaf module, `protocol-write-gates-tables.ts`. No behaviour changes and nothing to do on upgrade.
- The published `index.d.ts` and `index.d.cts` carry the same public surface; only docblocks of private members change, where a moved body took its docblock with it.
2 changes: 1 addition & 1 deletion docs/qa/platform-checklist/areas/platform-core.json
Original file line number Diff line number Diff line change
Expand Up @@ -2068,7 +2068,7 @@
"examples/app-showcase/src/data/extensions/account.extension.ts#AccountExtension (AccountExtension — the shipped fixture, priority 210)",
"packages/objectql/src/registry.ts#mergeObjectDefinitions (mergeObjectDefinitions — additive fields fold, the spread) + vicinity via engine: packages/objectql/src/engine.ts#registerApp (the objectExtensions loop registering 'extend' contributors — re-pointed #18104: objectExtensions is read off the manifest here, a member access; the loop this item means runs inside registerApp, which this file declares)",
"packages/spec/src/data/object.zod.ts#ObjectExtensionSchema (ObjectExtensionSchema — what an extension may carry, the #4001 silently-dropped history, 'Fields to add/override', priority wins-on-conflict; defineObjectExtension)",
"packages/metadata-protocol/src/protocol.ts#refusal (the NOT_OVERRIDABLE refusal: a packaged item is sealed by its managed package, ADR-0131 D6)",
"packages/metadata-protocol/src/protocol-write-gates.ts#refusal (the NOT_OVERRIDABLE refusal: a packaged item is sealed by its managed package, ADR-0131 D6)",
"packages/plugins/plugin-auth/src/managed-extension-fields.ts + managed-extension-fields.test.ts (the build-time collision gate — better-auth-managed objects ONLY; adopted by ADR-0126 §3 as Regime E prior art, NOT a generic gate)",
"docs/adr/0126-packaged-metadata-customization-model.md §2-§3 (Regime E: additive contributions at package grain, base never edited, install-grain not per-org)",
"content/docs/capabilities/integrations.mdx + content/docs/build-without-code.mdx (the E1 docs claim this item grounds)",
Expand Down
24 changes: 23 additions & 1 deletion packages/metadata-protocol/src/protocol-class-head.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* `resolveCascadeDeleteObjects`, `registerPublishMaterializer`,
* `registerUninstallCleanup`, `runUninstallCleanups`,
* `registerMutationProjector`, `registerAuthoringGate`,
* `registerCredentialChannel`, `restoreWriteFor`,
* `registerCredentialChannel`, `restoreWriteFor`, `objectExtensionItemRefusal`,
* `registerStoredObjectExtension`, `resyncStoredObjectExtensions`,
* `onMetadataMutation`, `emitMetadataMutation`, `attachMetadataMutationPubSub`,
* `detachMetadataMutationPubSub`, `applyRemoteMetadataMutation`,
Expand Down Expand Up @@ -60,6 +60,7 @@ import type {
MetadataMutationProjector,
} from './protocol-mutation-events.js';
import type { MetadataAuthoringGate, MetadataCredentialChannel } from './protocol-authoring-channel.js';
import { objectExtensionItemRefusal } from './protocol-object-extension-item.js';
import type { ProtocolHost } from './protocol-host.js';

export class ProtocolClassHeadFacet {
Expand Down Expand Up @@ -201,6 +202,27 @@ export class ProtocolClassHeadFacet {
return { intent, deriveRestoredBody };
}

/**
* [#28574, ADR-0146 D10] The save, publish and restore doors' gate for an
* `object_extension` body written into `binding` (the RESOLVED package of
* the row): {@link objectExtensionItemRefusal}, asked with this engine's
* registry and the package's dependency closure
* ({@link resolveWritePackageScope}). Any other type answers `undefined`.
*/
static objectExtensionItemRefusal(host: Pick<ProtocolHost, 'engine' | 'resolveWritePackageScope'>, type: string,
item: unknown,
name: string,
binding: string | null | undefined,): Error | undefined {
if ((PLURAL_TO_SINGULAR[type] ?? type) !== OBJECT_EXTENSION_TYPE) return undefined;
return objectExtensionItemRefusal(OBJECT_EXTENSION_TYPE, {
item,
name,
binding,
registry: host.engine?.registry as ObjectExtensionRegistry | undefined,
scope: host.resolveWritePackageScope(binding),
});
}

/**
* [#28574, ADR-0146 D10] Register a stored `object_extension` row as its
* package's `'extend'` contributor of the object it names — the WHOLE
Expand Down
82 changes: 81 additions & 1 deletion packages/metadata-protocol/src/protocol-discovery-and-bases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
/**
* [#29985 codemod] The "Discovery, types, diagnostics, packaged bases,
* credential carry-forward" facet of `ObjectStackProtocolImplementation`: the
* bodies of `getProjectId`, `getMetaTypes`, `getPackagedObjectBase`,
* bodies of `getProjectId`, `getMetaTypes`,
* `rethrowUnlessMetadataStoreUnprovisioned`, `getPackagedObjectBase`,
* `getPackagedDashboardBase`, `getPackagedViewBase`, `getPackagedReportBase`,
* `packagedArtifactBase`, `objectRegistrationRefusal`,
* `restoreTranslatedObjectEchoes`, `restoreInjectedColumnEchoes`,
Expand Down Expand Up @@ -55,6 +56,8 @@ import {
toJsonSchemaSafe,
} from './protocol-meta-type-schema.js';
import type { SaveMetaItemRequest } from './protocol-door-requests.js';
import { isMissingTableError } from '@objectstack/metadata/errors';
import { metadataReadFailureError } from './protocol-client-failures.js';
import type { ProtocolHost } from './protocol-host.js';
import { servedOverrideBaseOf } from './served-override.js';
import type { ObjectStackProtocolImplementation } from './protocol.js';
Expand Down Expand Up @@ -233,6 +236,83 @@ export class ProtocolDiscoveryAndBasesFacet {
return listing;
}

/**
* [#5532] Decide what a failed READ against `sys_metadata` means, and
* rethrow unless it is the ONE benign reason.
*
* ## The defect
*
* Every overlay read in `getMetaItems`/`getMetaItem` used to `catch {}` into
* its own empty value — `items` left as-is, `item` left `undefined`, the
* draft lookup falling through to the active read. That made a metadata
* store the protocol cannot reach **indistinguishable** from an environment
* where the item was never customised, and the emptiness then travelled
* the whole read chain unremarked:
*
* - `getMetaItemCached` turned it into `not found` — an outage answered
* as "that item does not exist", which is the opposite disposition
* (retry the backend vs. create the item / fix the link);
* - the `state='draft'` read turned it into `NO_DRAFT` / 404 — an outage
* answered as "there is no pending edit", which a publish flow reads as
* "nothing to publish";
* - `getMetaItems` turned it into `items: []` — an outage answered as
* "this environment declares none of these", the exact shape #5108 fixed
* one layer down in `DatabaseLoader` and #5089 in `listForIndex`.
*
* ADR-0110 D3 is the rule: a miss and an outage are different facts with
* opposite meanings, and a consumer must never read one as the other.
*
* ## The one benign reason
*
* `sys_metadata` has not been provisioned yet. There are then genuinely no
* overlay rows, so falling through to the registry / MetadataService IS the
* truth, and a first boot must not explode. Classification is by error TYPE
* through {@link isMissingTableError} — the same predicate `DatabaseLoader`
* (#5108) and this package's own `SysMetadataRepository` (#4867) ask, so a
* driver quirk is taught to the platform once. Conservative in the same
* direction: an unrecognised error is NOT benign, because a false "benign"
* silently mis-answers "does this exist?" while a false "real" costs one
* 503 the caller can retry.
*
* ## [#12536] Not benign is not the same as "the store is down"
*
* A non-benign failure is classified once more before it is wrapped, by
* {@link metadataReadFailureError}: a metadata app's hook may have REFUSED
* this read and marked its refusal with `userMessage` (commit 79c46da90), and that is
* an application refusal, not a dependency outage. Handing it to the 503
* below destroyed the mark at the producer — the two failures left this
* guard as the same envelope with the same sentence. The classification
* splits them; neither category's wording changes.
*
* @throws {@link markedApplicationRefusalError} — [#12536] FIRST, when the
* failure carried a producer's `userMessage` mark (commit 79c46da90): the
* author's text verbatim, in its own refusal category, with the
* cause still carried for the operator and nothing else quoted.
* @throws {@link metadataStoreUnavailableError} — a 503 carrying the driver
* error as `cause`, for every UNMARKED non-benign failure. Not the
* driver error itself: unwrapped, it has
* no status, so the REST boundary would have to guess from the
* message text — and `mapDataError` guesses `no such table` into
* `404 OBJECT_NOT_FOUND`, i.e. straight back into a miss.
* @returns normally ONLY for the benign case, licensing the caller to treat
* the overlay as absent.
*/
static rethrowUnlessMetadataStoreUnprovisioned(error: unknown, readObject: string): void {
// [commit 4cda78c9b] `readObject` is REQUIRED, deliberately. This helper serves
// callers that read four different tables (`sys_metadata`,
// `sys_metadata_audit`, `sys_metadata_commit`, `sys_metadata_history`),
// so a default would silently answer about the wrong one for three of
// them — measured, not hypothetical: the first draft of this repair
// hardcoded `sys_metadata` and turned `diffMetaItem`'s genuinely
// unprovisioned `sys_metadata_history` into a loud failure. A required
// parameter makes the compiler ask the question at every new call site.
if (isMissingTableError(error, readObject)) return;
// [#12536] CLASSIFY, do not assume. A read can fail because the store
// is unreachable OR because a metadata app's hook refused it in its
// own words — see {@link metadataReadFailureError}.
throw metadataReadFailureError(error);
}

/** The body of `ObjectStackProtocolImplementation#getPackagedObjectBase`; the method's docblock there is its contract. */
static getPackagedObjectBase(host: Pick<ProtocolHost, 'engine'>, name: string): unknown {
if (typeof name !== 'string' || name === '') return undefined;
Expand Down
2 changes: 2 additions & 0 deletions packages/metadata-protocol/src/protocol-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export interface ProtocolHost {
readonly isHostCodeDatasource: ObjectStackProtocolImplementation['isHostCodeDatasource'];
readonly isNestedArtifactField: ObjectStackProtocolImplementation['isNestedArtifactField'];
readonly isOverlayAllowed: (typeof ObjectStackProtocolImplementation)['isOverlayAllowed'];
readonly isPackagedBaseRefusal: (typeof ObjectStackProtocolImplementation)['isPackagedBaseRefusal'];
readonly isRuntimeCreateAllowed: (typeof ObjectStackProtocolImplementation)['isRuntimeCreateAllowed'];
readonly isSealedManagedItem: ObjectStackProtocolImplementation['isSealedManagedItem'];
readonly isStoredEntryOfDeclinedName: ObjectStackProtocolImplementation['isStoredEntryOfDeclinedName'];
Expand All @@ -163,6 +164,7 @@ export interface ProtocolHost {
readonly organizationScopedWriteRefusal: (typeof ObjectStackProtocolImplementation)['organizationScopedWriteRefusal'];
readonly originGatedRemovalRefusal: ObjectStackProtocolImplementation['originGatedRemovalRefusal'];
readonly overlaidShippedContainerViewNames: ObjectStackProtocolImplementation['overlaidShippedContainerViewNames'];
readonly overlayBaseShippers: ObjectStackProtocolImplementation['overlayBaseShippers'];
readonly overlayLockLayerAt: ObjectStackProtocolImplementation['overlayLockLayerAt'];
readonly overlayPackageMismatchError: (typeof ObjectStackProtocolImplementation)['overlayPackageMismatchError'];
readonly overlayRepos: ObjectStackProtocolImplementation['overlayRepos'];
Expand Down
Loading
Loading