@lotics/cli 0.69.0 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,7 +26,9 @@ export declare function generateAppWorkflowsDts(workflows: Record<string, AppWor
26
26
  * to `unknown` rather than throwing — the server's deploy-time schema parse
27
27
  * catches structurally-invalid declarations before they reach us.
28
28
  */
29
- export declare function inputsToType(inputs: Record<string, unknown>): string;
29
+ export declare function inputsToType(inputs: Record<string, unknown>, opts?: {
30
+ nullableOptional?: boolean;
31
+ }): string;
30
32
  /**
31
33
  * Map an output `fields` map (also the top-level `outputs`) to a TS object type.
32
34
  * Mirrors the backend `outputObjectToTsType`; recursive for nested object/array.
@@ -41,7 +41,7 @@ declare module "@lotics/app-sdk" {
41
41
  // No typed inputs ⇒ untyped `Record<string, unknown>` callable. An alias with
42
42
  // declared `outputs` also gets an `AppWorkflowResults` entry → typed result.data.
43
43
  const valueType = declaration.inputs
44
- ? inputsToType(declaration.inputs)
44
+ ? inputsToType(declaration.inputs, { nullableOptional: true })
45
45
  : "Record<string, unknown>";
46
46
  const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
47
47
  inputLines.push(` ${aliasKey}: ${valueType};`);
@@ -74,16 +74,27 @@ ${inputLines.join("\n")}
74
74
  * to `unknown` rather than throwing — the server's deploy-time schema parse
75
75
  * catches structurally-invalid declarations before they reach us.
76
76
  */
77
- export function inputsToType(inputs) {
77
+ export function inputsToType(inputs, opts) {
78
+ // A WORKFLOW optional input also accepts `null` — the diff-write "clear this
79
+ // field" signal (the backend validates it, the body forwards it to
80
+ // `update_records({ set: { fld: null } })`). So `useWorkflow` call sites can
81
+ // pass `null` to unset a field without a loose `Record<string, unknown>` cast.
82
+ // Query params + agent inputs have no "clear", so they stay `T` (the caller
83
+ // omits the flag). Only the TOP level is nullable: a nested object's optional
84
+ // field mirrors the backend's plain `.optional()` (the recursive call below
85
+ // omits the flag), so the type never claims a nested clear the server rejects.
86
+ const nullableOptional = opts?.nullableOptional === true;
78
87
  const fields = [];
79
88
  for (const [key, decl] of Object.entries(inputs)) {
80
89
  if (decl === null || typeof decl !== "object")
81
90
  continue;
82
91
  const d = decl;
83
92
  const tsType = inputDeclToTsType(d);
84
- const optional = d.required === false ? "?" : "";
93
+ const isOptional = d.required === false;
94
+ const fieldType = isOptional && nullableOptional ? `${tsType} | null` : tsType;
95
+ const optional = isOptional ? "?" : "";
85
96
  const fieldKey = isValidIdentifier(key) ? key : JSON.stringify(key);
86
- fields.push(` ${fieldKey}${optional}: ${tsType};`);
97
+ fields.push(` ${fieldKey}${optional}: ${fieldType};`);
87
98
  }
88
99
  if (fields.length === 0)
89
100
  return "Record<string, never>";
@@ -58,6 +58,32 @@ describe("generateAppWorkflowsDts", () => {
58
58
  expect(dts).toContain("note?: string;");
59
59
  expect(dts).toContain("ref: string;");
60
60
  });
61
+ it("types a TOP-LEVEL optional workflow input as `T | null` (the diff-write clear signal), required stays `T`, nested stays plain optional", () => {
62
+ const dts = generateAppWorkflowsDts({
63
+ updateTask: {
64
+ workflow_id: "wfl_u",
65
+ inputs: {
66
+ record_id: { type: "record_link", table_id: "tbl_x" },
67
+ due: { type: "date", required: false },
68
+ status: { type: "select", required: false, options: [{ label: "Done", value: "opt_done" }] },
69
+ tags: { type: "select", multi: true, required: false, options: [{ label: "A", value: "opt_a" }] },
70
+ meta: {
71
+ type: "object",
72
+ required: false,
73
+ fields: { note: { type: "text", required: false } },
74
+ },
75
+ },
76
+ },
77
+ });
78
+ expect(dts).toContain("record_id: string;"); // required → NOT nullable
79
+ expect(dts).toContain("due?: string | null;"); // optional scalar → clearable
80
+ expect(dts).toContain('status?: "opt_done" | null;');
81
+ expect(dts).toContain('tags?: ReadonlyArray<"opt_a"> | null;'); // optional multi
82
+ // The object itself is a top-level optional → nullable; its NESTED optional
83
+ // field mirrors the backend's plain `.optional()` (no `| null`).
84
+ expect(dts).toContain("note?: string;");
85
+ expect(dts).not.toContain("note?: string | null;");
86
+ });
61
87
  it("maps record_link/select outputs the same way as inputs", () => {
62
88
  const dts = generateAppWorkflowsDts({
63
89
  lookup: {
package/dist/src/cli.js CHANGED
@@ -31759,7 +31759,7 @@ declare module "@lotics/app-sdk" {
31759
31759
  const inputLines = [];
31760
31760
  const resultLines = [];
31761
31761
  for (const [alias, declaration] of entries) {
31762
- const valueType = declaration.inputs ? inputsToType(declaration.inputs) : "Record<string, unknown>";
31762
+ const valueType = declaration.inputs ? inputsToType(declaration.inputs, { nullableOptional: true }) : "Record<string, unknown>";
31763
31763
  const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
31764
31764
  inputLines.push(` ${aliasKey}: ${valueType};`);
31765
31765
  if (declaration.outputs) {
@@ -31778,15 +31778,18 @@ ${inputLines.join("\n")}
31778
31778
  }
31779
31779
  `;
31780
31780
  }
31781
- function inputsToType(inputs) {
31781
+ function inputsToType(inputs, opts) {
31782
+ const nullableOptional = opts?.nullableOptional === true;
31782
31783
  const fields = [];
31783
31784
  for (const [key, decl] of Object.entries(inputs)) {
31784
31785
  if (decl === null || typeof decl !== "object") continue;
31785
31786
  const d = decl;
31786
31787
  const tsType = inputDeclToTsType(d);
31787
- const optional = d.required === false ? "?" : "";
31788
+ const isOptional = d.required === false;
31789
+ const fieldType = isOptional && nullableOptional ? `${tsType} | null` : tsType;
31790
+ const optional = isOptional ? "?" : "";
31788
31791
  const fieldKey = isValidIdentifier(key) ? key : JSON.stringify(key);
31789
- fields.push(` ${fieldKey}${optional}: ${tsType};`);
31792
+ fields.push(` ${fieldKey}${optional}: ${fieldType};`);
31790
31793
  }
31791
31794
  if (fields.length === 0) return "Record<string, never>";
31792
31795
  return `{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.69.0",
3
+ "version": "0.70.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {