Skip to content

Commit d70e94a

Browse files
fix(cloud-connection): an install-local install connects and records the package's declared datasources (#28132)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DNZYoA6dDo1ZK7uh4DbA5hi)_ Fixes #28039 Clause-②: yes (widening) Metadata-protocol: none Skills-docs: `content/docs/data-modeling/external-datasources.mdx` gains one callout under "When auto-connect fails" (an installed package's datasources are registered as code and connected at install and every restart; a failed one is installed with its failed `status`; the install answer's `datasources`; `409` on a held name). `skills/**`: none — `git grep -n -i "datasource" skills/objectstack-platform/references/operations.md` has no line on install-local and datasources; the install-local rows there (`:107`, `:344`) describe the CLI and the credential facts, which this change does not touch. ## 维护者速读 - **改了什么**:通过 marketplace 本地安装(`os package install`)装进运行中环境的包,现在会像启动其产物时一样,把它声明的数据源注册为代码数据源、记入代码数据源名单并自动连接;卸载时一并撤回。启动路径与安装路径共用一个 runtime 助手 `connectPackageDatasources`。 - **为什么改**:此前安装只注册对象、不管数据源,绑定到包自带数据源的对象读写一律 404 `OBJECT_NOT_FOUND`,后台也不把它当代码数据源。 - **风险与回滚**:启动路径行为不变(既有 53 个数据源启动测试 + runtime 全套 7883 通过);新增两处对外行为:安装答复多一个 `datasources` 字段,包声明的数据源名若已被占用(`default`、后台建的、别的包的)则安装 409 拒绝。回滚即还原本 PR。 - **先看**:`packages/runtime/src/declared-datasources.ts`(助手本体,重点看 `installed` 分支吞掉 fail-fast 改为回报状态)、`packages/cloud-connection/src/marketplace-install-local-plugin.ts`(安装 4b′、重新加载、2a 冲突拒绝)、`packages/runtime/src/app-plugin.ts`(启动路径改为调用助手)。 - **两分钟验证**:`pnpm --filter @objectstack/dogfood exec vitest run test/install-local-declared-datasources.dogfood.test.ts`,预期 8 个用例全绿:安装后绑定对象可写可读,凭据无法解析的数据源读到 503 `ERR_DATASOURCE_UNAVAILABLE`,卸载后全部撤回。 - **需要你做的**:无,席位落地。凭据从哪来仍由 #27687 裁决,本 PR 不涉及。 ## What changes - **One path.** `AppPlugin.start`'s three datasource steps — the in-memory metadata registration as `origin: 'code'` stamped with the declaring package, the `connectDeclared` call through the `datasource-connection` service, and its fail-fast verdict — move into `connectPackageDatasources` (`packages/runtime/src/declared-datasources.ts`, exported from `@objectstack/runtime`). The boot calls it with the same lists by reference (#23029's identity pins hold), the same log lines and the same throw; its names still join the code-datasource set in `init()`. - **install-local calls it** on install (step 4b′, after the schema sync, only when the package hot-loaded) and on every `kernel:ready` rehydrate, with `installed: true`, which differs from the boot in three ways: the names are contributed to the code-datasource set now; a fail-fast verdict is logged and reported instead of thrown (the datasource keeps its retained verdict and the engine's unavailable mark, so a bound read answers `503 ERR_DATASOURCE_UNAVAILABLE` naming it); and a reinstall first withdraws what the previous version registered. - **Uninstall.** The helper registers, once per kernel, the `runtime.package-datasources` uninstall cleanup on the protocol's registry (the shape `runtime.package-jobs` uses), so install-local's `DELETE` and the protocol's `deletePackage` both close the pools, remove the metadata registrations still stamped with the package and take the names out of the set. It acts only on what an installed package's call recorded; a boot artifact records nothing. - **The install answer** carries `datasources: [{ name, status }]` — the connection service's own `ConnectStatus` names, nothing new — omitted for a package that declares none. - **Step 2a refusal.** A package declaring a datasource name another holder owns (the host's `default`, a datasource in the metadata service not stamped with this package) is refused `409 RESOURCE_CONFLICT` with `details.datasource`, before any write: the connect is idempotent on the name, so it would have served the package's objects from the holder's pool, and recording the name as code would have made the holder's own datasource read-only. - `CodeDatasourceNames` gains `delete`, and `code-datasource-names.ts` a `withdrawCodeDatasourceNames` (module-internal, not exported from the package); the module header now names the post-boot producer. ## Tests - `packages/qa/dogfood/test/install-local-declared-datasources.dogfood.test.ts` (new, real showcase composition, real sqlite driver): install answers both verdicts; registrations are `origin: 'code'` with `_packageId`/`_packageVersion`; the code-datasource set holds both names; the admin list serves `qads_store` `ok` and `qads_vault` `error` with a `statusReason` naming the credential; write `201` + read `200` of the bound object; the vault-bound read `503 ERR_DATASOURCE_UNAVAILABLE`; uninstall `cleanups` carries `runtime.package-datasources` `{ success: true, removed: 2 }` and leaves no verdict, registration or name (control: `showcase_external` stays); `showcase_external` and `default` collisions `409`; a restart's rehydrate reconnects and the object reads. - `packages/runtime/src/declared-datasources.test.ts` (new): the installed/boot split — names recorded or not, fail-fast reported or thrown, reinstall withdrawal, cleanup scoped to its package and to registrations still stamped with it, no connection service. ## Acceptance notes - **Measured on `main` `549203d0` first** (same dogfood fixture, source reverted): install `200` with `hotLoaded: true`, then `POST`/`GET /data/qads_note` and `GET /data/qads_secret_row` all `404 OBJECT_NOT_FOUND` ("Object 'qads_note' is not registered"); no connection verdict for either datasource; the code-datasource set held only `default` and `showcase_external`. - **The unresolved-credential status.** With an object bound explicitly the connection service's fail-fast path throws, and `connect()` retains that verdict as `failed-degraded` (reason naming the credential); without a bound object it is `failed-credentials`. Both are the service's existing names, and the admin list maps both to `error`. Recording `failed-credentials` on the fail-fast path too would be a change to `service-datasource`'s retained verdict on a refused boot; not made here. - **#27687 is not ruled.** The credential resolves the one way the connection service resolves every `credentialsRef`. The installed datasources now pass the round-3 gate #27687 measured (`trigger: 'declared-auto'` and `CODE_DATASOURCE_NAMES_SERVICE.has(name)`), so whatever it rules for code datasources reaches them. - **`datasourceMapping`** rules in an installed package are not installed by this door (before or after); the helper is called with no mapped objects, so D2 (d) never fires for an installed package. - **objectos `artifact-kernel-factory`** (outside this repo) composes its own kernels; whether it connects package datasources itself is not measured here. - **Out of scope, reported to the seat:** the `503` text for a `failed` datasource (`DatasourceUnavailableError`, `packages/objectql/src/driver-connect-errors.ts`) says it "failed to connect at startup and the server was started with OS_ALLOW_DRIVER_CONNECT_FAILURE", which is false for an install-time failure (measured on this PR's dogfood read of `qads_secret_row`) and, by the same branch, for a Setup-created datasource that failed. ## Verification <details><summary>Ablations — each leg red, then restored (<code>git diff HEAD</code> empty)</summary> All through `scripts/ablation-replace.mjs` (anchor 1 → 0, blob moved) under the verify lock. The dogfood file resolves `@objectstack/cloud-connection` and `@objectstack/runtime` to source (the main-side measurement above ran on reverted source with no rebuild), so no dist preflight applies. | Leg | Mutation | Result | |---|---|---| | A1 | install step 4b′ connect → `undefined` | dogfood 6 failed / 2 passed | | A2 | rehydrate connect removed | dogfood 1 failed (rehydrate) | | A3 | 2a refusal disabled | dogfood 1 failed (409 collisions) | | A4 | uninstall cleanup registration removed | dogfood 1 failed (uninstall withdrawal) | | A5 | `if (!installed) throw err` → `throw err` | unit 1 failed (fail-fast reported) | </details> <details><summary>Local runs at head <code>475a5292</code></summary> - Measurement on `main` `549203d0` (dogfood fixture, source reverted, then restored): `Tests 1 passed`, readings in Acceptance notes. - `pnpm --filter @objectstack/dogfood exec vitest run test/install-local-declared-datasources.dogfood.test.ts` — `Tests 8 passed (8)`. - `pnpm --filter @objectstack/runtime test` — `Test Files 484 passed (484)`, `Tests 7883 passed | 19 skipped (7902)`. - `pnpm --filter @objectstack/cloud-connection test` — `Test Files 61 passed (61)`, `Tests 758 passed (758)`. - `pnpm --filter @objectstack/runtime typecheck`, `pnpm --filter @objectstack/cloud-connection typecheck` — exit 0 (after `pnpm --filter @objectstack/runtime build`). - `node scripts/pm/dispatch-gates.mjs --commands`: 92 families, each run with its exit captured before any pipe; three exited 3 (prerequisite) on the first pass — `check-plugin-teardown-shape --self-test` (cleared by fetching the pinned commit), `spec check:skill-examples` and `check:dual-build-cjs-loads` (cleared by building `client-react`, `studio` and five plugin/service packages) — and exited 0 on re-run. `--ran`: `92 derived famil(ies) accounted for — 92 run, 0 NOT-MEASURED`. - `pnpm lint` — exit 0. - Dogfood battery: n/a (no `!` in the title, no `(narrowing)` arm). `test:repo` is CI's. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4cx4xgJksGo7iVA8c9A)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6ab6b72 commit d70e94a

9 files changed

Lines changed: 1094 additions & 100 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/runtime': patch
3+
'@objectstack/cloud-connection': patch
4+
---
5+
6+
fix(cloud-connection)!: an install-local install connects and records the datasources the package declares, as a boot of its artifact does
7+
8+
Clause-②: yes (narrowing)
9+
Metadata-protocol: none
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object or datasource definition, stored row or stored shape is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What narrows is the install-local door's accept set: a package whose declared datasource name is already held in the target environment is refused `409` where it was installed `200`. Whether a package is refused depends on what the environment already holds, not on the package's metadata, and the package's metadata stays valid as written; the remedy is the author's choice of a datasource name that is not taken, never a mechanical rewrite of a package source or a stored row. The other categories are closed on facts: both packages publish (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed, only added (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
12+
13+
**BREAKING** accept-set narrowing at `POST /api/v1/marketplace/install-local`, alongside the widening below (a new `@objectstack/runtime` export and a new field on the install answer).
14+
15+
- **Before.** A package declaring a datasource whose name the environment already held (the host's `default`, a datasource created in Setup, another package's) installed `200`, and its declaration was dropped: measured on `main` with a package declaring `showcase_external`, the install answered `200` with `hotLoaded: true`.
16+
- **Now.** The same install is refused `409 RESOURCE_CONFLICT`, with `details.datasource` naming the held name, before anything is written. Accepting it now would serve the package's objects from the holder's pool (the connect is idempotent on the name) and, by recording the name as code, make the holder's own datasource read-only.
17+
- **Unchanged.** A package that declares no datasource, or only names nothing else holds, installs as before; a reinstall of the same package is not a collision with itself.
18+
- **Who sees it.** Only an install-local caller (`os package install`, an AI agent, a script) installing a package whose datasource name collides with one the environment already has.
19+
20+
**The remedy.** Give the package's datasource a name the environment does not already hold, and install again.
21+
22+
**What else changes.**
23+
24+
A package installed through `POST /api/v1/marketplace/install-local` had its objects registered and nothing of its datasources: an object bound to the package's own datasource answered `404 OBJECT_NOT_FOUND` on every read and write, and the datasource was not served as code-defined. Now the install, and every restart's rehydrate of the ledger, take the path a boot of the package's artifact takes:
25+
26+
- **One path.** `@objectstack/runtime` exports `connectPackageDatasources`, which `AppPlugin.start` now calls for the booted artifact and install-local calls for an installed package. Each declared datasource is registered in the metadata registry as `origin: 'code'`, stamped with the package (`_packageId`, `_packageVersion`), its name is recorded in the host's code-datasource set, and it is connected through the `datasource-connection` service's `connectDeclared` (the ADR-0062 D2 gate and the host's connect policy apply as on a boot). The boot's own behaviour is unchanged.
27+
- **The install answer** carries `datasources`: each declared datasource's connect verdict (`connected`, or the connection service's own non-`ok` status). Omitted for a package that declares none.
28+
- **A credential that cannot be resolved** is that datasource's named non-`ok` verdict, never a driver gap: the package is installed, Setup → Datasources reports the datasource as `error` with its reason, and a read of an object bound to it answers `503 ERR_DATASOURCE_UNAVAILABLE` naming it. Where a code datasource's credential comes from is not changed here.
29+
- **Reinstall and uninstall.** A reinstall replaces the previous version's datasources; the uninstall closes their pools, removes their registrations and their names, reported as the `runtime.package-datasources` entry in `cleanups`.

‎content/docs/data-modeling/external-datasources.mdx‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,17 @@ connect, so queries against its objects keep failing until the server is
186186
restarted. Do not set it in production.
187187
</Callout>
188188

189+
<Callout type="info">
190+
**A package installed into a running runtime** (`os package install`, which calls
191+
`POST /api/v1/marketplace/install-local`) gets the same treatment at install time
192+
and at every restart: its declared datasources are registered as `origin: code`,
193+
read-only, and auto-connected by the same gates. There is no boot left to refuse,
194+
so a datasource that cannot be connected is installed with its failed `status`
195+
instead, and reads of its bound objects answer `503 ERR_DATASOURCE_UNAVAILABLE`.
196+
The install answer lists each datasource's verdict under `datasources`. A package
197+
declaring a datasource name the runtime already holds is refused `409`.
198+
</Callout>
199+
189200
### Seeing the state of a datasource
190201

191202
Every connect attempt's verdict is retained, so a datasource that is down no

‎packages/cloud-connection/src/marketplace-install-local-plugin.ts‎

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,19 @@ function manifestIdOf(p: any): string | undefined {
632632
return p?.manifest?.id ?? p?.id ?? p?.manifest?.name ?? undefined;
633633
}
634634

635+
/**
636+
* [#28039] The datasources a package body declares, as `AppPlugin` reads a
637+
* stack's: a list, or a map keyed by name.
638+
*/
639+
function declaredDatasources(body: unknown): any[] {
640+
const defs = isPlainObject(body) ? body.datasources : undefined;
641+
return Array.isArray(defs)
642+
? defs
643+
: isPlainObject(defs)
644+
? Object.entries(defs).map(([name, def]) => ({ name, ...(def as any) }))
645+
: [];
646+
}
647+
635648
/**
636649
* The seed datasets a cached manifest bundles: its `data[]` entries that name
637650
* an object and carry records. One reading for the two doors that match seed
@@ -1009,6 +1022,10 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
10091022
const ql: any = ctx.getService('objectql');
10101023
if (ql && typeof ql.syncSchemas === 'function') await ql.syncSchemas();
10111024
} catch { /* non-fatal */ }
1025+
// [#28039] Connect the package's declared datasources — the
1026+
// boot artifact's were connected in `AppPlugin.start`, and this
1027+
// package arrives after it.
1028+
await this.connectDeclaredDatasources(ctx, converted, entry.manifestId, entry.version);
10121029
// [#21321] Bind the package's script-action bodies and body
10131030
// hooks — `register` above makes them declared, not runnable.
10141031
await this.bindArtifactHandlers(ctx, converted, entry.manifestId);
@@ -1788,6 +1805,27 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
17881805
}, 409);
17891806
}
17901807

1808+
// 2a. [#28039] The package's datasources are registered as code and
1809+
// connected in step 4b′, so a name another holder already owns —
1810+
// the host's `default`, a datasource created in Setup or another
1811+
// package's — is refused here, before any write: connecting would
1812+
// hand the package's objects that holder's pool (the connect is
1813+
// idempotent on the name), and recording the name as code would
1814+
// make the holder's own datasource read-only to the admin door.
1815+
const heldDatasource = await this.findHeldDatasourceName(ctx, manifest, manifestId);
1816+
if (heldDatasource) {
1817+
return c.json({
1818+
success: false,
1819+
error: {
1820+
code: 'RESOURCE_CONFLICT',
1821+
message: `Package '${manifestId}' cannot be installed: it declares the datasource '${heldDatasource.name}', `
1822+
+ `and ${heldDatasource.holder} already holds that name in this system. Rename the package's datasource, `
1823+
+ 'or remove the one holding the name first.',
1824+
details: { datasource: heldDatasource.name },
1825+
},
1826+
}, 409);
1827+
}
1828+
17911829
// 2b. [ADR-0120 D5e] `isolated`-posture gate on installation-wide
17921830
// uniques. Runs BEFORE hot-register and before anything is written
17931831
// to the ledger: a stopped install must leave the runtime exactly as
@@ -1967,6 +2005,16 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
19672005
ctx.logger?.warn?.(`[MarketplaceInstallLocal] syncSchemas failed for ${manifestId}: ${err?.message ?? err}`);
19682006
}
19692007

2008+
// 4b′. [#28039] Register, record and connect the package's declared
2009+
// datasources through the runtime's ONE path — the call
2010+
// `AppPlugin.start` makes for a boot artifact — after the schema
2011+
// sync, where the boot connects them too. Only for a package the
2012+
// running kernel holds: on the lenient path nothing of it is
2013+
// registered until the restart's rehydrate, which connects them.
2014+
const datasources = hotLoadError === undefined
2015+
? await this.connectDeclaredDatasources(ctx, converted, manifestId, version)
2016+
: undefined;
2017+
19702018
// 4c. [#21321] Bind the package's script-action bodies and body hooks
19712019
// through the runtime's ONE binder — the call `AppPlugin.start`
19722020
// makes for a boot artifact. A reinstall replaces the previous
@@ -2026,6 +2074,11 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
20262074
// [#27237] Omitted, not emptied, for a package whose connectors
20272075
// name no credential, so that answer's key set is unchanged.
20282076
...(credentialRefs !== undefined ? { credentialRefs } : {}),
2077+
// [#28039] Each declared datasource's connect verdict — `connected`,
2078+
// or the connection service's named non-`ok` status (a credential
2079+
// it cannot resolve is `failed-credentials`). Omitted for a package
2080+
// that declares none, so that answer's key set is unchanged.
2081+
...(datasources !== undefined ? { datasources } : {}),
20292082
...(unsetRefs.length > 0 ? { credentialNote: installedCredentialNote(unsetRefs, { door }) } : {}),
20302083
upgradedFrom: conflict === 'marketplace' ? 'previous-marketplace-version' : null,
20312084
translationsLoaded: seededSummary.translationsLoaded,
@@ -3477,6 +3530,111 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
34773530
return typeof ql?.convertPackageBodyForward === 'function' ? ql.convertPackageBodyForward(manifest) : manifest;
34783531
};
34793532

3533+
/**
3534+
* [#28039] Register, record and connect the datasources `manifest`
3535+
* declares, through `connectPackageDatasources` — the ONE path, which
3536+
* `AppPlugin.start` takes for the artifact the host boots: each registered
3537+
* in the metadata registry as `origin: 'code'` stamped with this package,
3538+
* its name recorded in the host's code-datasource set, and connected
3539+
* through the `datasource-connection` service (the D2 gate and the host's
3540+
* connect policy apply as on a boot). A reinstall replaces the previous
3541+
* version's datasources; the uninstall withdraws them through the
3542+
* `runtime.package-datasources` uninstall cleanup.
3543+
*
3544+
* Before this, the door registered a package's objects and nothing of its
3545+
* datasources: an object bound to one had no driver, and the admin door
3546+
* did not serve the datasource as code-defined.
3547+
*
3548+
* ⛔ Where a declared credential comes from is not decided here (#27687):
3549+
* a credential the connection service cannot resolve is its
3550+
* `failed-credentials` verdict, which the datasource answers and a read of
3551+
* a bound object names (`ERR_DATASOURCE_UNAVAILABLE`).
3552+
*
3553+
* Answers each declared datasource's verdict, or `undefined` when the
3554+
* package declares none (or the runtime predates the path). Never throws:
3555+
* the package is installed either way.
3556+
*/
3557+
private connectDeclaredDatasources = async (
3558+
ctx: PluginContext,
3559+
manifest: unknown,
3560+
manifestId: string,
3561+
version: string | undefined,
3562+
): Promise<Array<{ name: string; status: string }> | undefined> => {
3563+
const declared = declaredDatasources(manifest);
3564+
let connect: typeof import('@objectstack/runtime')['connectPackageDatasources'] | undefined;
3565+
try {
3566+
const mod: any = await import('@objectstack/runtime');
3567+
if (typeof mod?.connectPackageDatasources === 'function') connect = mod.connectPackageDatasources;
3568+
} catch { /* reported below */ }
3569+
if (!connect) {
3570+
if (declared.length > 0) {
3571+
ctx.logger?.warn?.(
3572+
`[MarketplaceInstallLocal] this runtime has no connectPackageDatasources — the datasources of ${manifestId} are NOT connected: `
3573+
+ 'the objects bound to them have no driver. Upgrade @objectstack/runtime alongside @objectstack/cloud-connection.',
3574+
);
3575+
}
3576+
return undefined;
3577+
}
3578+
const objects = isPlainObject(manifest) && Array.isArray(manifest.objects) ? manifest.objects : [];
3579+
try {
3580+
// Called with an empty list too: a reinstall whose new version
3581+
// declares none withdraws what the previous version registered.
3582+
const verdicts = await connect(ctx, {
3583+
appId: manifestId,
3584+
source: 'MarketplaceInstallLocal',
3585+
datasources: declared,
3586+
owned: declared.map((datasource) => ({
3587+
datasource,
3588+
owner: { packageId: manifestId, ...(typeof version === 'string' ? { packageVersion: version } : {}) },
3589+
})),
3590+
objects,
3591+
installed: true,
3592+
});
3593+
return declared.length > 0 ? verdicts : undefined;
3594+
} catch (err: any) {
3595+
ctx.logger?.error?.(
3596+
`[MarketplaceInstallLocal] the datasources of ${manifestId} could not be registered or connected: ${err?.message ?? err}`,
3597+
);
3598+
return declared.length > 0 ? [] : undefined;
3599+
}
3600+
};
3601+
3602+
/**
3603+
* [#28039] The first datasource `manifest` declares whose name another
3604+
* holder already owns, with that holder: the host's `default`, or a
3605+
* datasource in the metadata service not stamped with this package (one
3606+
* created in Setup, the boot artifact's, another package's). A datasource
3607+
* this package's earlier install registered is its own, so a reinstall
3608+
* passes. `undefined` when nothing collides or nothing can be read.
3609+
*/
3610+
private findHeldDatasourceName = async (
3611+
ctx: PluginContext,
3612+
manifest: unknown,
3613+
manifestId: string,
3614+
): Promise<{ name: string; holder: string } | undefined> => {
3615+
const names = declaredDatasources(manifest)
3616+
.map((ds) => ds?.name)
3617+
.filter((name): name is string => typeof name === 'string' && name.length > 0);
3618+
if (names.length === 0) return undefined;
3619+
if (names.includes('default')) return { name: 'default', holder: "the host's primary datasource" };
3620+
let metadata: { get?: (type: string, name: string) => Promise<unknown> } | undefined;
3621+
try { metadata = ctx.getService('metadata'); } catch { return undefined; }
3622+
for (const name of names) {
3623+
let item: unknown;
3624+
try { item = await metadata?.get?.('datasource', name); } catch { continue; }
3625+
if (!isPlainObject(item)) continue;
3626+
const owner = item._packageId;
3627+
if (owner === manifestId) continue;
3628+
return {
3629+
name,
3630+
holder: typeof owner === 'string' && owner.length > 0
3631+
? `the package '${owner}'`
3632+
: item.origin === 'runtime' ? 'a datasource created in Setup' : 'a datasource',
3633+
};
3634+
}
3635+
return undefined;
3636+
};
3637+
34803638
/**
34813639
* [#21321] Bind an installed package's executable handlers — its
34823640
* `type: 'script'` action bodies and its body hooks — through

0 commit comments

Comments
 (0)