Repository navigation
Conversation
|
Thanks for the contribution! Unfortunately we can't verify the commit author(s): Aparna Goyal <r***@A***.com>. One possible solution is to add that email to your GitHub account. Alternatively you can change your commits to another email and force push the change. After getting your commits associated with your GitHub account, sign the Salesforce Inc. Contributor License Agreement and this Pull Request will be revalidated. |
Add SDR support for Data Cloud dataspace-scoped metadata (CalculatedInsight, DataModelObject). Their on-disk shape differs from every existing type: each component is a pair of generic .json files - a payload <name>.json and a <name>.meta.json sidecar - nested under a d360/dataspace/<dataspace>/<typeDir>/ wrapper, with no -meta.xml. The component fullName is dataspace-scoped (<dataspace>.<name>). - registry: register the two types with dataspaceScoped adapter/transformer strategies and index the `json` suffix so bare .json files resolve as metadata. - resolve: DataspaceScopedSourceAdapter derives the <dataspace>.<name> fullName from the path and folds the .meta.json sidecar into its payload component. - convert: DataspaceScopedMetadataTransformer preserves the full d360/... path in both directions and co-writes the sidecar alongside the payload. - collections: expandDataspaceScopedComponentSet builds the minimal deploy closure from each component's sidecar retrieveWith set.
aparnagoy
force-pushed
the
d360-dataspace-scoped-metadata
branch
from
September 21, 2026 21:23
e8279ce to
8f238d7
Compare
added 5 commits
September 23, 2026 02:00
…package.xml Add DataLakeObject, DataspaceMember, DataModelObjectMapping, SecondaryIndex, DataStreamBundle and DataConnection, and use the plural directory names the Metadata API writes (calculatedInsights, dataModelObjects, ...). Add an excludeFromManifest registry flag and ComponentSet.addWithoutManifestEntry so components that only deploy as part of another one travel as files without their own package.xml entry.
Accept <dataspace>.<name> on deploy for components stored outside any dataspace (e.g. DataLakeObject:default.myDlo__dll, DataStreamBundle:default.myStream), the same name retrieve accepts. The local component is resolved by its bare name, package.xml keeps the requested name, and the retrieveWith closure is limited to that dataspace. Match retrieveWith entries by type and name, so components of different types that share a name are told apart, and honour the dataspace a reference names.
A component outside any dataspace, such as a Data Lake Object, is stored once but can be retrieved through several dataspaces. On retrieve, its payload is overwritten and its .meta.json is merged with the local copy: retrieveWith entries of the dataspaces the retrieve covers are replaced, and those of other dataspaces are kept. On deploy for one dataspace, the closure skips entries that name another dataspace, and the sidecar is sent with retrieveWith trimmed to the requested dataspaces. The local file is left unchanged.
A Data Stream bundle names its Data Lake Object with the dataspace it uses it in, but the DLO is stored outside every dataspace, so its sidecar has none. The closure only matched a peer whose dataspace equalled the one the reference named, so it dropped the DLO and the deploy failed for a missing component. A reference that names a dataspace now also matches a peer stored outside every dataspace.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat: support Data Cloud (D360) dataspace-scoped metadata types
Summary
Adds first-class SDR support for Data Cloud dataspace-scoped metadata types.
These types do not fit any existing adapter/transformer because their on-disk
representation is unlike every other metadata type:
.jsonfiles (there is no-meta.xml):‹name›.json— the payload (the component definition).‹name›.meta.json— a sidecar carrying relationship metadata(
componentType,componentName,dataspaceName,retrieveWith,dependsOn).d360/dataspace/‹dataspace›/‹typeDir›/, and their fullName isdataspace-scoped:
‹dataspace›.‹name›(e.g.‹ComponentType›:‹dataspace›.‹name›),where
‹dataspace›is the path segment immediately above the type directory.Stream bundles) live directly under
d360/‹typeDir›/and keep their bare‹name›.They can still be requested for one dataspace as
‹dataspace›.‹name›(see below).d360/…path is preserved verbatim in both directions.Registered types
package.xmlCalculatedInsightcalculatedInsightsDataModelObjectdataModelObjectsDataLakeObjectdataLakeObjectsDataStreamBundledataStreamBundlesDataConnectiondataConnectionsDataspaceMemberdataspaceMembersDataModelObjectMappingdataModelObjectMappingsSecondaryIndexsecondaryIndexesDirectory names are the plural folder names the Metadata API writes, like every
other source-format folder (
classes/,objects/).The registry, resolver, converter, and deploy-closure logic are taught to handle
that shape end-to-end, in both retrieve (metadata → source) and deploy
(source → metadata) directions.
The runtime pieces are type-agnostic: they derive everything from the path
shape and the sidecar convention, so onboarding an additional dataspace-scoped
type is a registry change (a type entry plus a directory mapping), never a
code change. This keeps the design open for extension and closed for
modification — new types plug in without touching the adapter, transformer, or
closure logic.
Design
Three new pieces of behavior, wired through the existing strategy indirection so
no core class is modified. All three share the single adapter/transformer
strategy id
d360:metadataRegistry.jsond360adapter + transformer strategies; index thejsonsuffix so bare.jsonfiles are recognized as metadata; strict directory names disambiguate one type's‹typeDir›from another's.D360SourceAdapter‹dataspace›.‹name›fullName from the path; treat the.meta.jsonsidecar as part of its sibling payload component (never a component of its own).D360MetadataTransformerd360/…path in both directions and co-write the sidecar alongside the payload.expandD360ComponentSetretrieveWithset.resolveDataspaceQualifiedComponents‹dataspace›.‹name›deploy request for a component stored outside any dataspace to its local bare-named source.ComponentSet.addWithoutManifestEntry+ registryexcludeFromManifestpackage.xmlentry.Only the requested components go in
package.xmlThe Metadata API deploys
retrieveWithpeers as part of the component thatpulls them in; some peer types (
DataspaceMember,DataModelObjectMapping,SecondaryIndex) are rejected as manifest entries of their own. So the closureadds peers with
addWithoutManifestEntry: their files travel in the zip, whilepackage.xmllists just what the user asked for — the same as on retrieve. Theregistry flag
excludeFromManifestkeeps those peer-only types out of themanifest in every other code path too.
Deploying for one dataspace:
‹dataspace›.‹name›Retrieve accepts
DataLakeObject:default.myDlo__dllbecause the org resolves thename. Locally that DLO is stored once, as
myDlo__dll, so the same name on deployused to match no source. Now:
ComponentSetBuilderresolves the request to the localmyDlo__dll(
resolveDataspaceQualifiedComponents).package.xmlkeeps the requested name,default.myDlo__dll.defaultdataspace: peers in other dataspaces areleft out, including entries that name another dataspace. Requesting the bare
name still walks every dataspace.
retrieveWithtrimmed to thedefaultpeers(and peers outside every dataspace), so the org isn't asked for peers the
deploy doesn't carry. The local file is left as is.
One sidecar for every dataspace
A component outside any dataspace is stored once, but it can be retrieved through
several dataspaces, each returning its own
retrieveWith. Retrievingdefault.myDlo__dlland thenother.myDlo__dllmust not lose thedefaultpeers:‹name›.json) is overwritten with the latest definition.‹name›.meta.json) is merged with the one already in theproject: the new
retrieveWithreplaces the entries of the dataspaces it covers,and entries of other dataspaces are kept. Everything else comes from the new sidecar.
So the sidecar can list peers from several dataspaces, and deploy (above) sends
only the requested dataspace's share of it.
Matching
retrieveWithentriesEach entry is
{ componentType, name, dataspaceName? }. Peers are matched bytype and name against their sidecars, because different types can share a
name (a DLO and its
DataspaceMemberare both named after the DLO). When anentry names a dataspace, the peer in that dataspace matches, and so does one
stored outside every dataspace. A Data Stream bundle, for example, names its
DLO with the dataspace it uses it in, while the DLO itself is stored bare.
Why the
jsonsuffix index mattersResolution gates every file through
isMetadata(), which callsregistry.getTypeBySuffix(extName(path)). A suffix that is not present in theglobal
suffixesindex is silently skipped, so bare.jsonfiles were neverconsidered metadata. Indexing
jsonis what lets these components resolve atall;
resolveTypethen disambiguates the concrete type per-directory via thestrict folder name (each type's registered
directoryName), so a single sharedjsonsuffix mapping serves every dataspace-scoped type.Sidecar handling and the name-collision guard
baseName()splits on the first., so‹name›.jsonand‹name›.meta.jsonboth reduce to
‹name›.D360SourceAdapter.populate()therefore mustexclude
*.meta.json; otherwise the sidecar would resolve to the samedataspace-scoped name as the payload and collide. The sidecar is not lost — the
transformer writes it next to the payload, merged with the local copy on retrieve
(see above).
retrieveWithvsdependsOnThe sidecar declares two distinct relationship lists:
retrieveWith— the components that travel together with this one as asingle deployable unit. This is what the deploy closure walks.
dependsOn— informational metadata about what a component references(the components it points at). It is not used to build the closure.
End-to-end flow (across repos)
The stack is always user → CLI → SDR → Core (server). The CLI is the command
entry point, SDR is the library that resolves/converts/packages the components
and computes the deploy closure, and Core owns the on-disk/wire contract (it
emits the two-file layout and the server-populated
retrieveWithon retrieve,and validates/applies the unit on deploy). SDR carries no per-type logic — the
d360strategy is selected purely from the registry.plugin-deploy-retrieveretrieveWithclosure before handing off to SDR.source-deploy-retrieve(this PR)D360SourceAdapter+D360MetadataTransformerthat resolve/convert the two-file layout both directions, andexpandD360ComponentSetwhich computes the deploy closure.d360/…payload +.meta.jsonsidecar and populates each component'sretrieveWith. On deploy validates and applies the unit.What crosses the SDR boundary
ComponentSet+ org connectionD360SourceAdapter), converts metadata → source (D360MetadataTransformer.toSourceFormat), co-writes the.meta.jsonsidecar, merging itsretrieveWithwith the local copyRetrieveResult+FileResponse[]fullprojectComponentSet+requestedComponentSet(+ registry)d360members by sidecar type +componentName, walksretrieveWithtransitively (limited to the requested dataspace, if any), trims shared sidecars to that dataspaceComponentSet(requested in the manifest + closure as files)ComponentSet+ org connectionD360MetadataTransformer.toMetadataFormat), packages the zip, submits the Metadata API deploy, pollsDeployResultsf project retrieve start— metadata → sourcesequenceDiagram actor User participant CLI as CLI (plugin-deploy-retrieve) participant SDR as SDR (source-deploy-retrieve) participant Core as Core (server) User->>CLI: sf project retrieve start --metadata ‹Type›:‹dataspace›.‹name› CLI->>SDR: ComponentSetBuilder.build(requested) → componentSet.retrieve(org) SDR->>Core: Metadata API retrieve request Core-->>SDR: unpackaged.zip (payload + .meta.json sidecar,<br/>server-populated retrieveWith) SDR->>SDR: resolve (D360SourceAdapter) + convert to source (D360MetadataTransformer.toSourceFormat) SDR-->>CLI: RetrieveResult + FileResponse[] CLI-->>User: writes force-app/main/default/d360/dataspace/…<br/>(payload + sidecar), prints resultCall stack inside SDR (retrieve):
sf project deploy start— source → metadatasequenceDiagram actor User participant CLI as CLI (plugin-deploy-retrieve) participant SDR as SDR (source-deploy-retrieve) participant Core as Core (server) User->>CLI: sf project deploy start --source-dir … | --metadata ‹Type›:‹dataspace›.‹name› CLI->>SDR: ComponentSetBuilder.build(requested) SDR->>SDR: resolveDataspaceQualifiedComponents (‹dataspace›.‹name› → local ‹name›) SDR-->>CLI: requested ComponentSet CLI->>CLI: hasD360Components(requested)? CLI->>SDR: ComponentSetBuilder.build(whole project) → full CLI->>SDR: expandD360ComponentSet(full, requested, registry) SDR-->>CLI: requested + transitive retrieveWith closure CLI->>SDR: componentSet.deploy(org) (MetadataApiDeploy) SDR->>Core: Metadata API deploy (payload + sidecar under d360/…) Core-->>SDR: DeployResult SDR-->>CLI: DeployResult CLI-->>User: prints resultCall stack inside SDR (deploy):
Call stack (resolution & conversion detail)
Resolution (retrieve):
Conversion (both directions):
Deploy closure:
Testing
yarn build(compile + lint) — green.yarn test(unit suite) — green.test/collections/d360Dependencies.test.ts,componentSet.test.ts) cover:‹dataspace›.‹name›) inpackage.xml;‹dataspace›.‹name›to the local component;the bundle names the DLO with a dataspace and the DLO is stored outside every dataspace;
test/convert/transformers/d360MetadataTransformer.test.ts) covermerging a retrieved sidecar into the local one (entries of other dataspaces kept,
those of a covered dataspace replaced), writing it as is when there's no local copy,
and reading from a tree that can't read synchronously, as a retrieve zip can't.
excludeFromManifest/addWithoutManifestEntry.sf project retrieve startwrites the payload +.meta.jsonsidecar for aretrieved component and its
retrieveWithpeers underforce-app/main/default/d360/…,and the reverse deploy direction reproduces the same layout in the mdapi package.
sf project deploy start --metadata "DataLakeObject:default.‹name›"deploys theDLO with only its
defaultpeers, withpackage.xmllistingdefault.‹name›.