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
2 changes: 1 addition & 1 deletion docs/docs/api/appkit/Class.ResourceRegistry.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 27 additions & 6 deletions packages/appkit/src/registry/resource-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ export class ResourceRegistry {
* const result = registry.validate();
*
* if (!result.valid) {
* console.error("Missing resources:", result.missing.map(r => Object.values(r.fields).map(f => f.env)));
* console.error(ResourceRegistry.formatMissingResources(result.missing));
* }
* ```
*/
Expand Down Expand Up @@ -392,7 +392,7 @@ export class ResourceRegistry {
type: r.type,
alias: r.alias,
plugin: r.plugin,
envVars: Object.values(r.fields).map((f) => f.env),
envVars: ResourceRegistry.unsetEnvVars(r),
})),
},
});
Expand All @@ -410,6 +410,25 @@ export class ResourceRegistry {
return validation;
}

/**
* Env var names for an entry's fields that are actually unset (undefined or
* empty), skipping fields that declare no env var (e.g. discovery-only
* fields resolved at deploy time). Mirrors the resolution check in
* {@link validate} so error output lists exactly what the caller must set —
* not every field on the resource.
*/
private static unsetEnvVars(entry: ResourceEntry): string[] {
const unset: string[] = [];
for (const field of Object.values(entry.fields)) {
if (!field.env) continue;
const val = process.env[field.env];
if (val === undefined || val === "") {
unset.push(field.env);
}
}
return unset;
}

/**
* Formats missing resources into a human-readable error message.
*
Expand All @@ -422,8 +441,8 @@ export class ResourceRegistry {
}

const lines = missing.map((entry) => {
const envVars = Object.values(entry.fields).map((f) => f.env);
const envHint = ` (set ${envVars.join(", ")})`;
const envVars = ResourceRegistry.unsetEnvVars(entry);
const envHint = envVars.length > 0 ? ` (set ${envVars.join(", ")})` : "";
return ` - ${entry.type}:${entry.alias} [${entry.plugin}]${envHint}`;
});

Expand All @@ -444,11 +463,13 @@ export class ResourceRegistry {
];

for (const entry of missing) {
const envVars = Object.values(entry.fields).map((f) => f.env);
const envVars = ResourceRegistry.unsetEnvVars(entry);
contentLines.push(
` ${entry.type}:${entry.alias} (plugin: ${entry.plugin})`,
);
contentLines.push(` Set: ${envVars.join(", ")}`);
if (envVars.length > 0) {
contentLines.push(` Set: ${envVars.join(", ")}`);
}
}

contentLines.push("");
Expand Down
71 changes: 71 additions & 0 deletions packages/appkit/src/registry/tests/resource-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,77 @@ describe("ResourceRegistry", () => {
else delete process.env.DATABRICKS_WAREHOUSE_ID;
}
});

it("should list only the unset env vars, not the ones already set", () => {
const prevScope = process.env.SECRET_SCOPE;
const prevKey = process.env.SECRET_KEY;
process.env.SECRET_SCOPE = "my-scope";
delete process.env.SECRET_KEY;
try {
const registry = new ResourceRegistry();
registry.register("analytics", {
type: ResourceType.SECRET,
alias: "creds",
resourceKey: "creds",
description: "Credentials",
permission: "READ",
required: true,
fields: {
scope: { env: "SECRET_SCOPE" },
key: { env: "SECRET_KEY" },
},
});

const result = registry.validate();
expect(result.valid).toBe(false);

const formatted = ResourceRegistry.formatMissingResources(
result.missing,
);
// Only the genuinely-missing var is reported; the one already set is not.
expect(formatted).toContain("SECRET_KEY");
expect(formatted).not.toContain("SECRET_SCOPE");
} finally {
if (prevScope !== undefined) process.env.SECRET_SCOPE = prevScope;
else delete process.env.SECRET_SCOPE;
if (prevKey !== undefined) process.env.SECRET_KEY = prevKey;
else delete process.env.SECRET_KEY;
}
});

it("should omit fields that declare no env var (e.g. discovery-only fields)", () => {
const prevHost = process.env.PGHOST_TEST;
delete process.env.PGHOST_TEST;
try {
const registry = new ResourceRegistry();
registry.register("lakebase", {
type: ResourceType.POSTGRES,
alias: "Postgres",
resourceKey: "postgres",
description: "Lakebase Postgres",
permission: "CAN_CONNECT_AND_CREATE",
required: true,
fields: {
// Discovery-only field: no env var, resolved at deploy time.
project: { description: "Project resource name" },
host: { env: "PGHOST_TEST" },
},
});

const result = registry.validate();
const formatted = ResourceRegistry.formatMissingResources(
result.missing,
);
// The one real missing env var is present…
expect(formatted).toContain("PGHOST_TEST");
// …and there are no stray empty entries from the env-less field.
expect(formatted).not.toContain("(set , ");
expect(formatted).not.toContain(", )");
} finally {
if (prevHost !== undefined) process.env.PGHOST_TEST = prevHost;
else delete process.env.PGHOST_TEST;
}
});
});

describe("collectResources with getResourceRequirements", () => {
Expand Down
Loading