@objectstack/metadata-protocol@16.0.0
Minor Changes
-
bfa3c3f: Broadcast a
transactionalBatchcapability bit in discovery so clients negotiate the atomic cross-object batch declaratively, instead of runtime-probing 404/405/501 (#3298).The atomic cross-object batch endpoint (
POST {basePath}/batch, #1604 / ADR-0034 item 4) and its typed SDK surface (client.data.batchTransaction, #3271) already shipped, but discovery never told a client whether a backend actually supports it. Consumers (notably ObjectUI'sObjectStackAdapter) had to probe: fire a/batch, read404/405(no route) or501(no runtime transaction), and only then fall back to non-atomic client-side simulation. That is "find out by calling", not capability negotiation — it cannot be decided at connect time and cannot serve as the "minimum backend supports/batch" gate that blocks hard-deleting the non-atomic fallback downstream.WellKnownCapabilitiesSchemagains a requiredtransactionalBatch: boolean, and every discovery producer fills it honestly (declared === enforced), so it never becomes a declared-but-unpopulated bit:@objectstack/metadata-protocol(getDiscovery) — reports whether the runtime engine can honour a transaction (typeof engine.transaction === 'function'). The/batchhandler runs its ops insideengine.transaction(), which degrades to a non-atomic passthrough (or 501) without one.@objectstack/rest(/discovery) — ANDs the engine signal with whether it actually mounts the route (api.enableBatch), so a server with batch disabled reportsfalseeven on a transaction-capable engine (never advertise an endpoint that would 404).@objectstack/plugin-hono-server(standalone discovery) — reportsfalse: this minimal surface registers CRUD only and does not mount/batch(that ships with@objectstack/rest). Under-reporting is the safe direction — a client keeps its correct-but-slower fallback rather than losing atomicity.@objectstack/client— already normalizes hierarchicalcapabilitiesto flat booleans, soclient.capabilities.transactionalBatchis exposed (and now typed) for declarative consumers.
The bit follows the existing capability semantics:
true⟺ the/batchroute is mounted and the runtime can honour a transaction — the exact condition under which the endpoint returns200rather than404/405/501. Additive and behavior-preserving; only the discovery payload gains a field. -
668dd17: Breaking (npm type surface): retire the vestigial feed contracts + protocol surface (ADR-0052 §5 follow-up, #1959).
The
service-feedruntime was deleted in #1955;sys_comment/sys_activity
are the canonical record-collaboration/timeline backend. This removes the dead
type surface that still pointed at the deleted runtime — every removed method was
already unreachable (the feed REST route was never mounted → 404; the protocol
implementation was never wired with a feed service, sorequireFeedService()
could only throw). No behavior changes.No authorable metadata key is removed (the
feeds:object capability flag and
theRecordActivityUI component config are unchanged), soPROTOCOL_MAJOR
stays 15 and this ships asminorrather than a protocol major.FROM → TO migration for every removed export:
@objectstack/spec/contracts—IFeedService,CreateFeedItemInput,
UpdateFeedItemInput,ListFeedOptions,FeedListResult→ removed, no
replacement. Comments/activity are plain records: writesys_comment/ read
sys_activityvia the data engine or the REST data API.@objectstack/spec/api—FeedApiContracts,FeedApiErrorCode,
FeedProtocol, and all feed request/response schemas + types (GetFeed*,
CreateFeedItem*,UpdateFeedItem*,DeleteFeedItem*,AddReaction*,
RemoveReaction*,PinFeedItem*,UnpinFeedItem*,StarFeedItem*,
UnstarFeedItem*,SearchFeed*,GetChangelog*,ChangelogEntry,
SubscribeRequest/Response,FeedUnsubscribeRequest,UnsubscribeResponse,
FeedPathParams,FeedItemPathParams,FeedListFilterType) → removed. Use
the data API againstsys_comment/sys_activity(/api/v1/data/sys_comment/…);
reactions and threaded replies are fields onsys_comment.@objectstack/spec/data—FeedItemSchema/FeedItem,FeedActorSchema/FeedActor,
MentionSchema/Mention,ReactionSchema/Reaction,
FieldChangeEntrySchema/FieldChangeEntry,FeedVisibility,
RecordSubscriptionSchema/RecordSubscription,SubscriptionEventType, and the
data-namespaceNotificationChannel→ removed.FeedItemTypeand
FeedFilterModeare kept (live UI activity-timeline config). For notification
channels useNotificationChannelSchemafrom@objectstack/spec/system.@objectstack/client—client.feed.*(list/create/update/delete/
addReaction/removeReaction/pin/unpin/star/unstar/search/
getChangelog/subscribe/unsubscribe) and the re-exported feed response
types → removed. One-line fix: useclient.data.*onsys_comment/
sys_activity, e.g.client.data.create('sys_comment', { object, record_id, body })
andclient.data.find('sys_activity', { filters: [['record_id', '=', id]] }).@objectstack/metadata-protocol—ObjectStackProtocolImplementationno longer
implements the 14 feed methods; its constructor
(engine, getServicesRegistry?, getFeedService?, environmentId?)becomes
(engine, getServicesRegistry?, environmentId?). One-line fix: delete the third
argument.
Patch Changes
-
e057f42: fix: harden the bulk-write path — retries, idempotency, contracts, and summary visibility (#3147–#3152)
Six reliability fixes to the batched seed/import +
engine.insert(array)path
introduced by the #2678 bulk-write rework:- #3151
bulkWritevalidates thatwriteBatchreturns one record per input
row (a short/long/non-array return is degraded per-row, not backfilled as
phantom success);engine.insert(array)likewise rejects a short driver
bulkCreatereturn instead of padding afterInsert withundefined. - #3150 wraps the two remaining un-retried write points (seed
writeRecord/resolveDeferredUpdates, import's no-createManyData
fallback) inwithTransientRetry;defaultIsTransientErrorshort-circuits
definitive logical errors to non-transient. - #3148 import
resolveRefflushes pending creates on a same-object miss so
a later row can reference an earlier same-file CREATE, and no longer
negatively caches a miss. - #3149 threads an
attemptcounter throughbulkWrite; seed rechecks by
externalIdand import bymatchFieldsbefore re-writing, so a
commit-then-lost-response retry cannot duplicate a batch. - #3147
recomputeSummariesretries transient failures and, on exhaustion,
surfacesSummaryRecomputeError(ERR_SUMMARY_RECOMPUTE) instead of a
silent warn; seed/import recover it to a warning without re-writing. - #3152 autonumbers are assigned after validation, so a batch that dies in
validation consumes no sequence value (no number-range gaps).
- #3151
-
0e41302: fix(metadata-protocol): unscoped metadata list dedupes package-aware, not by bare name (ADR-0048 #1828)
getMetaItemsmerged registry items,sys_metadataoverlay rows, draft-preview
rows, and MetadataService items intoMaps keyed by barename, so two installed
packages shipping the sametype/name(e.g.page/home) collapsed to one row
(last-write-wins) on an unscopedGET /meta/:typewhenever either package had an
overlay — and the frontend prefer-local resolution, which reads that list, could
no longer tell the two packages' rows apart.The three merge sites (plus the env/org pre-merge) now key by
(package, name),
mirroringgetMetaItem's scoped-then-global-fallback resolution: colliding rows
stay distinct each with its own_packageId, a package-less (env-wide) overlay
still wins over the single artifact it customizes (ADR-0005 precedence and
single-package behaviour unchanged), and the registry-hydration artifact graft is
scoped to each row's ownpackage_idso a collision no longer mislabels provenance. -
b8a21ad: Publish/discard package drafts in the draft's own org scope, fixing
no_draftafter saving a draft via Studio.Studio "Save Draft" (
PUT /meta/:type/:name?mode=draft) never threads the session'sactiveOrganizationId, so the draft row is written env-wide (organization_id = NULL). "Publish" (POST /packages/:id/publish-drafts) resolves the active org and passed it topromoteDraft, which looked the draft up with a strictorganization_id = <org>equality — so it 404'd ([no_draft] No pending draft exists …) on the env-wide row it could never match, even thoughlistDraftshad already surfaced that draft to the publish CTA (PR #1852's$or).discardPackageDraftshad the same latent gap.listDraftsnow projects each draft's ownorganizationId, andpublishPackageDrafts/discardPackageDraftspromote / delete each draft in that scope (env-wide stays env-wide, per-org stays per-org). Seed-body capture and the ADR-0067 revert-plan pre-state read are scoped the same way.Fixes #3115.
-
beaf2de: fix(metadata-protocol): strip static
readonlyon INSERT at the data-write ingress (#3043)#2948/#3003 made static
readonly: truefields server-enforced on UPDATE (a
non-system PATCH forgingapproval_status: 'approved'is silently stripped in
the engine), but INSERT was exempt. For approval/status/verdict columns that
exemption was the shorter attack: instead of the #3003 draft-then-PATCH move, a
non-system caller couldPOSTa record alreadyapproval_status: 'approved'in
one step — and the UPDATE-only strip never reached it.The strip now also runs on INSERT, but at the external data-write ingress
(DataProtocol.createData/createManyData/batchData/cloneData) rather
than in the engine. That seam is the single point every external programmatic
create funnels through — the REST CRUD route, the GraphQL/MCP dispatcher
(bridge.create→callData→createData), and bulk import — while trusted
internal writers (better-auth's adapter, the metadata repository, the seed
loader) callengine.insertdirectly and bypass it. Enforcing at the ingress
protects every caller/agent path at once without stripping the internal writers
that legitimately seed read-only columns on create (identity provisioning,
provenance stamps, event-log cursors) — the blast radius an engine-level insert
strip would have.- Caller-forged only, at the ingress. The payload here is raw caller input
(the security middleware stampsowner_id/organization_idlater, inside
engine.insert), so only keys the caller actually sent are dropped; server
stamps are added afterwards and are unaffected. - Re-derives the default. A stripped field falls back to its declared
defaultValuein the engine (a forgedapproval_statusbecomesdraft, not
NULL). - System-context exempt.
isSystemwrites still seed read-only columns. - Silent (HTTP 2xx), per-row on batch/import.
readonlyWhenstays
INSERT-exempt (a conditional lock needs a prior record). - Author-defined business objects only. Platform objects (
managedByset,
or thesys_namespace) carry their own field-write governance that a silent
strip must not pre-empt — e.g. ADR-0086 REJECTS (403) a forged
managed_by:'package'onsys_permission_set, and #3004 rejects a forged
owner_id; several of those columns arereadonly, so stripping them here
would swallow the payload the guard is meant to reject. The #3043 threat is app
approval/status fields, neversys_— the same boundaryapplySystemFields
uses for ownership.
Behavior change: a non-system create through the data API (REST / GraphQL / MCP /
import) can no longer seed areadonlycolumn from the payload. Flows that
legitimately write read-only columns at creation must run with a system context
(isSystem), the same requirement the UPDATE strip already imposes. - Caller-forged only, at the ingress. The payload here is raw caller input
-
8abf133: Breaking (discovery response shape): retire the residual feed capability surface (#3180, follow-up to #1959 / ADR-0052 §5).
The feed backend was retired long ago; #1959 removed the feed contracts + SDK. This
removes the last discovery/dispatcher references to it, and fixes a real bug where the
commentscapability was permanentlyfalse.@objectstack/spec—WellKnownCapabilitiesSchema.feedandApiRoutesSchema.feed
(routes.feed) are removed, and the/api/v1/feedentry is dropped from
DEFAULT_DISPATCHER_ROUTES. FROM → TO: clients readingdiscovery.capabilities.feed
ordiscovery.routes.feed→ usediscovery.capabilities.comments; comments/activity
are served by the generic data API onsys_comment/sys_activity
(/api/v1/data/sys_comment/…).@objectstack/metadata-protocol—getDiscovery()no longer emits the always-false
feedservice/capability. Bug fix: thecommentscapability previously keyed off
the deleted'feed'service (so it was permanentlyfalseafter #1955); it now tracks
the presence of thesys_commentobject (provided by the always-on audit slate), so
declared === enforced.@objectstack/client— the internalfeed: '/api/v1/feed'route constant is removed
(it only existed to satisfy the now-removedApiRoutes.feedtype; no client code used it).
-
515f11a: fix(seed): replaying seeds no longer corrupts lookup natural keys on the upsert update path
Every dev-server restart replayed package seeds in upsert mode, and any record whose
lookup/master_detail was authored as a natural key could have that reference overwritten
with NULL on the update path (NOT NULL constraint failedon required columns; silent
link loss on nullable ones). Four fixes:- An unresolved reference now leaves the column untouched (deferred to pass 2) or drops
the record loudly — it is never written as NULL over an existing row. - DB-side reference resolution probes the target dataset's declared
externalId(e.g.
email) before falling back tonameandid, matching how in-memory resolution
already keyed records. - A rejected update (e.g. a
state_machinerule vetoing the replay) no longer severs
natural-key resolution for downstream child datasets. - Replays are idempotent: an upsert/update whose declared fields already match the
existing row is skipped instead of rewritten (no moreupdated_atchurn or lifecycle
re-validation on every boot).
- An unresolved reference now leaves the column untouched (deferred to pass 2) or drops
-
Updated dependencies [f972574]
-
Updated dependencies [6289ec3]
-
Updated dependencies [22013aa]
-
Updated dependencies [3ad3dd5]
-
Updated dependencies [8efa395]
-
Updated dependencies [3a18b60]
-
Updated dependencies [a8aa34c]
-
Updated dependencies [e057f42]
-
Updated dependencies [a3823b2]
-
Updated dependencies [43a3efb]
-
Updated dependencies [524696a]
-
Updated dependencies [6b51346]
-
Updated dependencies [80273c8]
-
Updated dependencies [bfa3c3f]
-
Updated dependencies [5e3301d]
-
Updated dependencies [dd9f223]
-
Updated dependencies [46e876c]
-
Updated dependencies [7125007]
-
Updated dependencies [5f05de2]
-
Updated dependencies [021ba4c]
-
Updated dependencies [158aa14]
-
Updated dependencies [62a2117]
-
Updated dependencies [83e8f7d]
-
Updated dependencies [d2723e2]
-
Updated dependencies [fefcd54]
-
Updated dependencies [beaf2de]
-
Updated dependencies [06cb319]
-
Updated dependencies [369eb6e]
-
Updated dependencies [06ff734]
-
Updated dependencies [b659111]
-
Updated dependencies [5754a23]
-
Updated dependencies [6c270a6]
-
Updated dependencies [290e2f0]
-
Updated dependencies [668dd17]
-
Updated dependencies [8abf133]
-
Updated dependencies [e0859b1]
-
Updated dependencies [92f5f19]
-
Updated dependencies [32899e6]
-
Updated dependencies [04ecd4e]
-
Updated dependencies [4d5a892]
-
Updated dependencies [16cebeb]
-
Updated dependencies [86d30af]
-
Updated dependencies [8923843]
-
Updated dependencies [ea32ec7]
-
Updated dependencies [a2795f6]
-
Updated dependencies [f16b492]
-
Updated dependencies [4b6fde8]
-
Updated dependencies [2018df9]
-
Updated dependencies [fc5a3a2]
-
Updated dependencies [8ff9210]
- @objectstack/spec@16.0.0
- @objectstack/core@16.0.0
- @objectstack/formula@16.0.0
- @objectstack/metadata-core@16.0.0
- @objectstack/types@16.0.0