Skip to content

Commit 2fa7e08

Browse files
authored
runner(schema): respect schema additionalProperties during validation (commontoolsinc#1809)
* runner(schema): respect schema additionalProperties during validation - Skip the additional-property traversal unless the schema explicitly enables additionalProperties, so we stop including all other keys with `any` schemas. - Emit a contextual warning if resolving a schema for an allowed additional key still returns undefined, making downstream schema drift visible. This fixes CT-897 by no longer overtriggering. * empty object implies additional properties * add tests * fix formatting * remove no longer needed as any cast * fix cli integration to no longer use path that is not in schema * clean up support for computed result schemas for charms
1 parent 5a0207a commit 2fa7e08

File tree

7 files changed

+203
-70
lines changed

7 files changed

+203
-70
lines changed

packages/charm/src/manager.ts

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -339,22 +339,7 @@ export class CharmManager {
339339
throw e;
340340
}
341341

342-
let resultSchema: JSONSchema | undefined = recipe?.resultSchema;
343-
344-
// If there is no result schema, create one from top level properties that omits UI, NAME
345-
if (!resultSchema) {
346-
const resultValue = charm.get();
347-
if (isObject(resultValue)) {
348-
resultSchema = {
349-
type: "object",
350-
properties: Object.fromEntries(
351-
Object.keys(resultValue).filter((key) => !key.startsWith("$")).map((
352-
key,
353-
) => [key, {}]), // Empty schema == any
354-
),
355-
};
356-
}
357-
}
342+
const resultSchema = this.#getResultSchema(charm, recipe);
358343

359344
if (runIt) {
360345
// Make sure the charm is running. This is re-entrant and has no effect if
@@ -369,6 +354,34 @@ export class CharmManager {
369354
}
370355
}
371356

357+
#getResultSchema(
358+
charm: Cell<unknown>,
359+
recipe: Recipe,
360+
): JSONSchema | undefined {
361+
if (
362+
isRecord(recipe.resultSchema) &&
363+
Object.keys(recipe.resultSchema).length > 0
364+
) return recipe.resultSchema;
365+
366+
// Ignore default cell schema to get to other values
367+
const resultValue = charm.asSchema().get();
368+
if (isObject(resultValue)) {
369+
const keys = Object.keys(resultValue).filter((key) =>
370+
!key.startsWith("$")
371+
);
372+
373+
// Only generate a schema for charms that have more than $ props
374+
if (keys.length > 0) {
375+
return {
376+
type: "object",
377+
properties: Object.fromEntries(keys.map((key) => [key, true])),
378+
};
379+
}
380+
}
381+
382+
return undefined;
383+
}
384+
372385
getLineage(charm: Cell<Charm>) {
373386
return charm.getSourceCell(charmSourceCellSchema)?.key("lineage").get() ??
374387
[];
@@ -786,10 +799,22 @@ export class CharmManager {
786799
if (!recipeId) throw new Error("charm missing recipe ID");
787800
const recipe = this.runtime.recipeManager.recipeById(recipeId);
788801
if (!recipe) throw new Error(`Recipe ${recipeId} not loaded`);
789-
// FIXME(ja): return should be Cell<Schema<T>> I think?
790802
return source.key("argument").asSchema<T>(recipe.argumentSchema);
791803
}
792804

805+
getResult<T = unknown>(
806+
charm: Cell<Charm | T>,
807+
): Cell<T> {
808+
const source = charm.getSourceCell(processSchema);
809+
const recipeId = source?.get()?.[TYPE]!;
810+
if (!recipeId) throw new Error("charm missing recipe ID");
811+
const recipe = this.runtime.recipeManager.recipeById(recipeId);
812+
if (!recipe) throw new Error(`Recipe ${recipeId} not loaded`);
813+
const resultSchema = this.#getResultSchema(charm, recipe);
814+
// FIXME(ja): return should be Cell<Schema<T>> I think?
815+
return charm.asSchema<T>(resultSchema);
816+
}
817+
793818
// note: removing a charm doesn't clean up the charm's cells
794819
// Now moves the charm to trash instead of just removing it
795820
async remove(idOrCharm: string | EntityId | Cell<Charm>) {

packages/charm/src/ops/charm-controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class CharmPropIo implements CharmCellIo {
6161
if (this.#type === "input") {
6262
return this.#cc.manager().getArgument(this.#cc.getCell());
6363
} else if (this.#type === "result") {
64-
return this.#cc.getCell();
64+
return this.#cc.manager().getResult(this.#cc.getCell());
6565
}
6666
throw new Error(`Unknown property type "${this.#type}"`);
6767
}

packages/cli/integration/integration.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,8 @@ test_value "Nested path set" "userData/user/name" '"Jane"' '"Jane"'
155155
echo "Testing --input flag operations..."
156156

157157
# Test input flag operations
158-
test_json_value "Input flag set" "config" '{"inputConfig":"test"}' "--input"
159-
test_value "Nested input path" "config/inputConfig" '"inputValue"' '"inputValue"' "--input"
158+
test_json_value "Input flag set" "userData" '{"user":{"name":"test"}}' "--input"
159+
test_value "Nested input path" "userData/user/name" '"inputValue"' '"inputValue"' "--input"
160160

161161
# Check space has new charm with correct inputs and title
162162
TITLE="Simple counter 2: 10"

packages/html/src/render.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export const renderImpl = (
9090
return cancel;
9191
}
9292
parent.append(root);
93-
logger.debug("Rendered", root);
93+
logger.debug("Rendered root", root);
9494
return () => {
9595
root.remove();
9696
cancel();

packages/runner/src/runner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1259,7 +1259,7 @@ export class Runner implements IRunner {
12591259
// TODO(seefeld): Make sure to not cancel after a recipe is elevated to a
12601260
// charm, e.g. via navigateTo. Nothing is cancelling right now, so leaving
12611261
// this as TODO.
1262-
addCancel(this.cancels.get(this.getDocKey(resultCell.getSourceCell()!)));
1262+
addCancel(() => this.stop(resultCell));
12631263
}
12641264
}
12651265

packages/runner/src/schema.ts

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { JSONSchemaObj } from "@commontools/api";
2+
import { getLogger } from "@commontools/utils/logger";
23
import { isObject, isRecord } from "@commontools/utils/types";
34
import { JSONSchemaMutable } from "@commontools/runner";
45
import { ContextualFlowControl } from "./cfc.ts";
@@ -16,6 +17,11 @@ import {
1617
} from "./query-result-proxy.ts";
1718
import { toCell, toOpaqueRef } from "./back-to-cell.ts";
1819

20+
const logger = getLogger("validateAndTransform", {
21+
enabled: true,
22+
level: "debug",
23+
});
24+
1925
/**
2026
* Schemas are mostly a subset of JSONSchema.
2127
*
@@ -677,23 +683,35 @@ export function validateAndTransform(
677683
}
678684

679685
// Handle additional properties if defined
680-
for (const key of keys) {
681-
if (!resolvedSchema.properties || !(key in resolvedSchema.properties)) {
682-
const childSchema = runtime.cfc.getSchemaAtPath(
683-
resolvedSchema,
684-
[key],
685-
rootSchema,
686-
);
687-
if (childSchema === undefined) {
688-
continue;
686+
if (resolvedSchema.additionalProperties || !resolvedSchema.properties) {
687+
for (const key of keys) {
688+
// Skip properties that were already processed above:
689+
if (!resolvedSchema.properties || !(key in resolvedSchema.properties)) {
690+
// Will use additionalProperties if present
691+
const childSchema = runtime.cfc.getSchemaAtPath(
692+
resolvedSchema,
693+
[key],
694+
rootSchema,
695+
);
696+
if (childSchema === undefined) {
697+
// This should never happen
698+
logger.warn(() => [
699+
"validateAndTransform: unexpected undefined schema for additional property",
700+
key,
701+
resolvedSchema,
702+
rootSchema,
703+
link,
704+
]);
705+
continue;
706+
}
707+
result[key] = validateAndTransform(
708+
runtime,
709+
tx,
710+
{ ...link, path: [...link.path, key], schema: childSchema },
711+
synced,
712+
seen,
713+
);
689714
}
690-
result[key] = validateAndTransform(
691-
runtime,
692-
tx,
693-
{ ...link, path: [...link.path, key], schema: childSchema },
694-
synced,
695-
seen,
696-
);
697715
}
698716
}
699717

0 commit comments

Comments
 (0)