@smithers-orchestrator/db 0.16.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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +43 -0
  3. package/src/JsonBounds.ts +6 -0
  4. package/src/SchemaRegistryEntry.ts +6 -0
  5. package/src/SqlMessageStorage.js +818 -0
  6. package/src/SqlMessageStorageEventHistoryQuery.ts +7 -0
  7. package/src/SqliteWriteRetryOptions.ts +7 -0
  8. package/src/adapter/AlertRow.ts +29 -0
  9. package/src/adapter/AlertSeverity.ts +2 -0
  10. package/src/adapter/AlertStatus.ts +2 -0
  11. package/src/adapter/ApprovalRow.ts +13 -0
  12. package/src/adapter/AttemptRow.ts +17 -0
  13. package/src/adapter/CacheRow.ts +12 -0
  14. package/src/adapter/DB_ALERT_ALLOWED_SEVERITIES.js +5 -0
  15. package/src/adapter/DB_ALERT_ALLOWED_STATUSES.js +6 -0
  16. package/src/adapter/DB_ALERT_ID_MAX_LENGTH.js +1 -0
  17. package/src/adapter/DB_ALERT_MESSAGE_MAX_LENGTH.js +1 -0
  18. package/src/adapter/DB_ALERT_POLICY_NAME_MAX_LENGTH.js +1 -0
  19. package/src/adapter/DB_RUN_ALLOWED_STATUSES.js +10 -0
  20. package/src/adapter/DB_RUN_ID_MAX_LENGTH.js +1 -0
  21. package/src/adapter/DB_RUN_WORKFLOW_NAME_MAX_LENGTH.js +1 -0
  22. package/src/adapter/EventHistoryQuery.ts +7 -0
  23. package/src/adapter/HumanRequestRow.ts +19 -0
  24. package/src/adapter/NodeDiffCacheRow.ts +9 -0
  25. package/src/adapter/NodeRow.ts +10 -0
  26. package/src/adapter/PendingHumanRequestRow.ts +7 -0
  27. package/src/adapter/RunAncestryRow.ts +5 -0
  28. package/src/adapter/RunRow.ts +21 -0
  29. package/src/adapter/SignalQuery.ts +6 -0
  30. package/src/adapter/SignalRow.ts +9 -0
  31. package/src/adapter/SmithersDb.js +2236 -0
  32. package/src/adapter/StaleRunRecord.ts +7 -0
  33. package/src/adapter/index.js +27 -0
  34. package/src/adapter.js +2359 -0
  35. package/src/assertJsonPayloadWithinBounds.js +94 -0
  36. package/src/assertMaxBytes.js +23 -0
  37. package/src/assertMaxJsonDepth.js +40 -0
  38. package/src/assertMaxStringLength.js +16 -0
  39. package/src/assertOptionalArrayMaxLength.js +16 -0
  40. package/src/assertOptionalStringMaxLength.js +11 -0
  41. package/src/assertPositiveFiniteInteger.js +14 -0
  42. package/src/assertPositiveFiniteNumber.js +12 -0
  43. package/src/buildHumanRequestId.js +9 -0
  44. package/src/cache/nodeDiffCache.js +124 -0
  45. package/src/ensure.js +18 -0
  46. package/src/ensureSqlMessageStorage.js +11 -0
  47. package/src/ensureSqlMessageStorageEffect.js +12 -0
  48. package/src/frame-codec/FRAME_KEYFRAME_INTERVAL.js +1 -0
  49. package/src/frame-codec/FrameDelta.ts +6 -0
  50. package/src/frame-codec/FrameDeltaOp.ts +20 -0
  51. package/src/frame-codec/FrameEncoding.ts +1 -0
  52. package/src/frame-codec/JsonPath.ts +3 -0
  53. package/src/frame-codec/JsonPathSegment.ts +1 -0
  54. package/src/frame-codec/applyFrameDelta.js +143 -0
  55. package/src/frame-codec/applyFrameDeltaJson.js +10 -0
  56. package/src/frame-codec/encodeFrameDelta.js +247 -0
  57. package/src/frame-codec/index.js +15 -0
  58. package/src/frame-codec/normalizeFrameEncoding.js +13 -0
  59. package/src/frame-codec/parseFrameDelta.js +27 -0
  60. package/src/frame-codec/serializeFrameDelta.js +9 -0
  61. package/src/frame-codec.js +409 -0
  62. package/src/getSqlMessageStorage.js +11 -0
  63. package/src/index.d.ts +5203 -0
  64. package/src/index.js +20 -0
  65. package/src/input-bounds.js +12 -0
  66. package/src/input.js +17 -0
  67. package/src/internal-schema/index.js +19 -0
  68. package/src/internal-schema/smithersAlerts.js +27 -0
  69. package/src/internal-schema/smithersApprovals.js +18 -0
  70. package/src/internal-schema/smithersAttempts.js +20 -0
  71. package/src/internal-schema/smithersCache.js +13 -0
  72. package/src/internal-schema/smithersCron.js +11 -0
  73. package/src/internal-schema/smithersEvents.js +10 -0
  74. package/src/internal-schema/smithersFrames.js +14 -0
  75. package/src/internal-schema/smithersHumanRequests.js +17 -0
  76. package/src/internal-schema/smithersNodeDiffs.js +12 -0
  77. package/src/internal-schema/smithersNodes.js +13 -0
  78. package/src/internal-schema/smithersRalph.js +10 -0
  79. package/src/internal-schema/smithersRuns.js +22 -0
  80. package/src/internal-schema/smithersSandboxes.js +16 -0
  81. package/src/internal-schema/smithersSignals.js +12 -0
  82. package/src/internal-schema/smithersTimeTravelAudit.js +12 -0
  83. package/src/internal-schema/smithersToolCalls.js +19 -0
  84. package/src/internal-schema/smithersVectors.js +12 -0
  85. package/src/internal-schema.js +245 -0
  86. package/src/isRetryableSqliteWriteError.js +53 -0
  87. package/src/loadInputEffect.js +28 -0
  88. package/src/loadOutputsEffect.js +87 -0
  89. package/src/output/OutputKey.ts +1 -0
  90. package/src/output/buildKeyWhere.js +17 -0
  91. package/src/output/buildOutputRow.js +34 -0
  92. package/src/output/describeSchemaShape.js +70 -0
  93. package/src/output/getAgentOutputSchema.js +13 -0
  94. package/src/output/getKeyColumns.js +19 -0
  95. package/src/output/index.js +14 -0
  96. package/src/output/selectOutputRowEffect.js +30 -0
  97. package/src/output/stripAutoColumns.js +10 -0
  98. package/src/output/upsertOutputRowEffect.js +38 -0
  99. package/src/output/validateExistingOutput.js +17 -0
  100. package/src/output/validateOutput.js +17 -0
  101. package/src/output-schema-descriptor.js +163 -0
  102. package/src/output.js +240 -0
  103. package/src/react-output.js +10 -0
  104. package/src/runState/ComputeRunStateOptions.ts +4 -0
  105. package/src/runState/DeriveRunStateInput.ts +10 -0
  106. package/src/runState/RUN_STATE_HEARTBEAT_STALE_MS.js +1 -0
  107. package/src/runState/ReasonBlocked.ts +10 -0
  108. package/src/runState/ReasonUnhealthy.ts +6 -0
  109. package/src/runState/RunState.ts +12 -0
  110. package/src/runState/RunStateView.ts +11 -0
  111. package/src/runState/computeRunState.js +22 -0
  112. package/src/runState/computeRunStateFromRow.js +102 -0
  113. package/src/runState/deriveRunState.js +109 -0
  114. package/src/runState/parseEventMeta.js +18 -0
  115. package/src/runState/parseTimerMeta.js +16 -0
  116. package/src/runState-types.ts +23 -0
  117. package/src/runState.js +7 -0
  118. package/src/schema-signature.js +22 -0
  119. package/src/snapshot.js +125 -0
  120. package/src/sql-message-storage.js +839 -0
  121. package/src/storage/InMemoryStorage.js +484 -0
  122. package/src/storage/StorageService.js +7 -0
  123. package/src/storage/StorageServiceShape.ts +122 -0
  124. package/src/storage/StorageServiceTypes.ts +150 -0
  125. package/src/unwrapZodType.js +17 -0
  126. package/src/utils/camelToSnake.js +6 -0
  127. package/src/withSqliteWriteRetryEffect.js +110 -0
  128. package/src/write-retry.js +49 -0
  129. package/src/zodToCreateTableSQL.js +41 -0
  130. package/src/zodToTable.js +60 -0
@@ -0,0 +1,28 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { getTableColumns } from "drizzle-orm/utils";
3
+ import { Effect } from "effect";
4
+ import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
5
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
6
+ /** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
7
+ /** @typedef {import("drizzle-orm").Table} Table */
8
+
9
+ /**
10
+ * @param {BunSQLiteDatabase<Record<string, never>>} db
11
+ * @param {Table} inputTable
12
+ * @param {string} runId
13
+ * @returns {Effect.Effect<Record<string, unknown> | undefined, SmithersError>}
14
+ */
15
+ export function loadInput(db, inputTable, runId) {
16
+ const cols = getTableColumns(inputTable);
17
+ const runIdCol = cols.runId;
18
+ if (!runIdCol) {
19
+ throw new SmithersError("DB_MISSING_COLUMNS", "schema.input must include runId column");
20
+ }
21
+ return Effect.tryPromise({
22
+ try: () => db.select().from(inputTable).where(eq(runIdCol, runId)).limit(1),
23
+ catch: (cause) => toSmithersError(cause, "load input", {
24
+ code: "DB_QUERY_FAILED",
25
+ details: { runId },
26
+ }),
27
+ }).pipe(Effect.map((rows) => rows[0]), Effect.annotateLogs({ runId }), Effect.withLogSpan("db:load-input"));
28
+ }
@@ -0,0 +1,87 @@
1
+ import { eq, getTableName } from "drizzle-orm";
2
+ import { getTableColumns } from "drizzle-orm/utils";
3
+ import { Effect, Option } from "effect";
4
+ import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
5
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
6
+ /** @typedef {import("@smithers-orchestrator/driver/OutputSnapshot").OutputSnapshot} OutputSnapshot */
7
+ /** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
8
+ /** @typedef {import("drizzle-orm").Table} Table */
9
+
10
+ /**
11
+ * @param {Table} table
12
+ * @returns {string[]}
13
+ */
14
+ function getBooleanColumnKeys(table) {
15
+ try {
16
+ const cols = getTableColumns(table);
17
+ const keys = [];
18
+ for (const [key, col] of Object.entries(cols)) {
19
+ const c = /** @type {Record<string, unknown> & { config?: { mode?: string }; mapFromDriverValue?: unknown }} */ (/** @type {unknown} */ (col));
20
+ const mapFn = /** @type {{ toString?: () => string } | undefined} */ (c?.mapFromDriverValue);
21
+ if (c?.columnType === "SQLiteBoolean" || c?.config?.mode === "boolean" || c?.mode === "boolean" || mapFn?.toString?.().includes("Boolean") || (c?.dataType === "boolean")) {
22
+ keys.push(key);
23
+ }
24
+ }
25
+ return keys;
26
+ } catch {
27
+ return [];
28
+ }
29
+ }
30
+ /**
31
+ * @param {ReadonlyArray<Record<string, unknown>>} rows
32
+ * @param {readonly string[]} boolKeys
33
+ * @returns {Array<Record<string, unknown>>}
34
+ */
35
+ function coerceBooleanColumns(rows, boolKeys) {
36
+ if (boolKeys.length === 0) return rows.slice();
37
+ return rows.map((row) => {
38
+ if (!row) return row;
39
+ /** @type {Record<string, unknown>} */
40
+ const patched = { ...row };
41
+ for (const key of boolKeys) {
42
+ if (key in patched && typeof patched[key] !== "boolean") {
43
+ patched[key] = Boolean(patched[key]);
44
+ }
45
+ }
46
+ return patched;
47
+ });
48
+ }
49
+ /**
50
+ * @param {BunSQLiteDatabase<Record<string, unknown>>} db
51
+ * @param {Record<string, Table | unknown>} schema
52
+ * @param {string} runId
53
+ * @returns {Effect.Effect<OutputSnapshot, SmithersError>}
54
+ */
55
+ export function loadOutputs(db, schema, runId) {
56
+ return Effect.gen(function* () {
57
+ /** @type {Record<string, ReadonlyArray<Record<string, unknown>>>} */
58
+ const out = {};
59
+ for (const [key, table] of Object.entries(schema)) {
60
+ if (!table || typeof table !== "object") continue;
61
+ if (key === "input") continue;
62
+ const colsOpt = yield* Effect.try({
63
+ try: () => getTableColumns(/** @type {Table} */ (table)),
64
+ catch: (cause) => toSmithersError(cause, "get table columns", { code: "DB_QUERY_FAILED", details: { runId, schemaKey: key } }),
65
+ }).pipe(Effect.option);
66
+ if (Option.isNone(colsOpt)) continue;
67
+ const cols = colsOpt.value;
68
+ const runIdCol = cols.runId;
69
+ if (!runIdCol) continue;
70
+ const tableNameOpt = yield* Effect.try({
71
+ try: () => getTableName(/** @type {Table} */ (table)),
72
+ catch: (cause) => toSmithersError(cause, "get table name", { code: "DB_QUERY_FAILED", details: { runId, schemaKey: key } }),
73
+ }).pipe(Effect.option);
74
+ if (Option.isNone(tableNameOpt)) continue;
75
+ const tableName = tableNameOpt.value;
76
+ const rawRows = yield* Effect.tryPromise({
77
+ try: () => db.select().from(/** @type {Table} */ (table)).where(eq(runIdCol, runId)),
78
+ catch: (cause) => toSmithersError(cause, `load outputs ${tableName}`, { code: "DB_QUERY_FAILED", details: { runId, tableName } }),
79
+ });
80
+ const boolKeys = getBooleanColumnKeys(/** @type {Table} */ (table));
81
+ const rows = coerceBooleanColumns(rawRows, boolKeys);
82
+ out[tableName] = rows;
83
+ out[key] = rows;
84
+ }
85
+ return /** @type {OutputSnapshot} */ (/** @type {unknown} */ (out));
86
+ }).pipe(Effect.annotateLogs({ runId }), Effect.withLogSpan("db:load-outputs"));
87
+ }
@@ -0,0 +1 @@
1
+ export type OutputKey = { runId: string; nodeId: string; iteration?: number };
@@ -0,0 +1,17 @@
1
+ import { and, eq } from "drizzle-orm";
2
+ import { getKeyColumns } from "./getKeyColumns.js";
3
+ /** @typedef {import("./OutputKey.ts").OutputKey} OutputKey */
4
+ /** @typedef {import("drizzle-orm").Table} Table */
5
+
6
+ /**
7
+ * @param {Table} table
8
+ * @param {OutputKey} key
9
+ */
10
+ export function buildKeyWhere(table, key) {
11
+ const cols = getKeyColumns(table);
12
+ const clauses = [eq(cols.runId, key.runId), eq(cols.nodeId, key.nodeId)];
13
+ if (cols.iteration) {
14
+ clauses.push(eq(cols.iteration, key.iteration ?? 0));
15
+ }
16
+ return and(...clauses);
17
+ }
@@ -0,0 +1,34 @@
1
+ import { getTableColumns } from "drizzle-orm/utils";
2
+ /** @typedef {import("drizzle-orm").Table} Table */
3
+
4
+ /**
5
+ * @param {Table} table
6
+ * @param {string} runId
7
+ * @param {string} nodeId
8
+ * @param {number} iteration
9
+ * @param {unknown} payload
10
+ */
11
+ export function buildOutputRow(table, runId, nodeId, iteration, payload) {
12
+ const cols = getTableColumns(table);
13
+ const keys = Object.keys(cols);
14
+ const hasPayload = keys.includes("payload");
15
+ const payloadOnly = hasPayload &&
16
+ keys.every((key) => key === "runId" ||
17
+ key === "nodeId" ||
18
+ key === "iteration" ||
19
+ key === "payload");
20
+ if (payloadOnly) {
21
+ return {
22
+ runId,
23
+ nodeId,
24
+ iteration,
25
+ payload: (payload ?? null),
26
+ };
27
+ }
28
+ return {
29
+ ...(payload ?? {}),
30
+ runId,
31
+ nodeId,
32
+ iteration,
33
+ };
34
+ }
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ import { getAgentOutputSchema } from "./getAgentOutputSchema.js";
3
+ /** @typedef {import("drizzle-orm").Table} Table */
4
+
5
+ /**
6
+ * @param {Table | z.ZodObject} tableOrSchema
7
+ * @param {z.ZodObject} [zodSchema]
8
+ * @returns {string}
9
+ */
10
+ export function describeSchemaShape(tableOrSchema, zodSchema) {
11
+ const schema = zodSchema ?? (isZodSchema(tableOrSchema) ? tableOrSchema : null);
12
+ if (schema && typeof schema.toJSONSchema === "function") {
13
+ const jsonSchema = schema.toJSONSchema();
14
+ return JSON.stringify(jsonSchema, null, 2);
15
+ }
16
+ if (!isZodSchema(tableOrSchema)) {
17
+ const agentSchema = getAgentOutputSchema(tableOrSchema);
18
+ if (typeof agentSchema.toJSONSchema === "function") {
19
+ const jsonSchema = agentSchema.toJSONSchema();
20
+ return JSON.stringify(jsonSchema, null, 2);
21
+ }
22
+ }
23
+ const target = schema ?? (isZodSchema(tableOrSchema) ? tableOrSchema : getAgentOutputSchema(tableOrSchema));
24
+ const shape = target.shape;
25
+ /** @type {Record<string, string>} */
26
+ const fields = {};
27
+ for (const [key, zodType] of Object.entries(shape)) {
28
+ fields[key] = describeZodType(/** @type {z.ZodType} */ (zodType));
29
+ }
30
+ return JSON.stringify(fields, null, 2);
31
+ }
32
+ /**
33
+ * @param {unknown} val
34
+ * @returns {val is z.ZodObject}
35
+ */
36
+ function isZodSchema(val) {
37
+ return (!!val && typeof val === "object" && "shape" in val && typeof (/** @type {{ shape: unknown }} */ (val)).shape === "object");
38
+ }
39
+ /**
40
+ * @param {z.ZodType} schema
41
+ * @returns {string}
42
+ */
43
+ function describeZodType(schema) {
44
+ const internal = /** @type {{ _zod?: { def?: Record<string, unknown> } }} */ (/** @type {unknown} */ (schema));
45
+ if (internal._zod?.def) {
46
+ const def = internal._zod.def;
47
+ const typeName = /** @type {string} */ (def.type);
48
+ if (typeName === "optional" || typeName === "default" || typeName === "nullable") {
49
+ const inner = def.innerType ? describeZodType(/** @type {z.ZodType} */ (def.innerType)) : "unknown";
50
+ if (typeName === "optional") return `${inner} (optional)`;
51
+ if (typeName === "nullable") return `${inner} | null`;
52
+ return inner;
53
+ }
54
+ if (typeName === "string") return "string";
55
+ if (typeName === "number" || typeName === "int" || typeName === "float") return "number";
56
+ if (typeName === "boolean") return "boolean";
57
+ if (typeName === "array") {
58
+ const itemType = def.element ? describeZodType(/** @type {z.ZodType} */ (def.element)) : "unknown";
59
+ return `${itemType}[]`;
60
+ }
61
+ if (typeName === "object") return "object";
62
+ if (typeName === "enum") return `enum(${(/** @type {unknown[]} */ (def.values ?? [])).join(" | ")})`;
63
+ if (typeName === "literal") return `literal(${JSON.stringify(def.value)})`;
64
+ if (typeName === "union") {
65
+ const options = (/** @type {z.ZodType[]} */ (def.options ?? [])).map((o) => describeZodType(o));
66
+ return options.join(" | ");
67
+ }
68
+ }
69
+ return "unknown";
70
+ }
@@ -0,0 +1,13 @@
1
+ import { createInsertSchema } from "drizzle-zod";
2
+ import { z } from "zod";
3
+ /**
4
+ * Creates a Zod schema for agent output by removing runId, nodeId, iteration
5
+ * (which are auto-populated by smithers)
6
+ */
7
+ export function getAgentOutputSchema(table) {
8
+ const baseSchema = createInsertSchema(table);
9
+ // Remove the key columns that smithers populates automatically
10
+ const shape = baseSchema.shape;
11
+ const { runId, nodeId, iteration, ...rest } = shape;
12
+ return z.object(rest);
13
+ }
@@ -0,0 +1,19 @@
1
+ import { getTableColumns } from "drizzle-orm/utils";
2
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
3
+ /** @typedef {import("drizzle-orm").AnyColumn} AnyColumn */
4
+ /** @typedef {import("drizzle-orm").Table} Table */
5
+
6
+ /**
7
+ * @param {Table} table
8
+ * @returns {{ runId: AnyColumn; nodeId: AnyColumn; iteration?: AnyColumn; }}
9
+ */
10
+ export function getKeyColumns(table) {
11
+ const cols = getTableColumns(table);
12
+ const runId = cols.runId;
13
+ const nodeId = cols.nodeId;
14
+ const iteration = cols.iteration;
15
+ if (!runId || !nodeId) {
16
+ throw new SmithersError("DB_MISSING_COLUMNS", `Output table ${table["_"]?.name ?? ""} must include runId and nodeId columns.`);
17
+ }
18
+ return { runId, nodeId, iteration };
19
+ }
@@ -0,0 +1,14 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("./OutputKey.ts").OutputKey} OutputKey */
3
+ // @smithers-type-exports-end
4
+
5
+ export { buildOutputRow } from "./buildOutputRow.js";
6
+ export { stripAutoColumns } from "./stripAutoColumns.js";
7
+ export { getKeyColumns } from "./getKeyColumns.js";
8
+ export { buildKeyWhere } from "./buildKeyWhere.js";
9
+ export { selectOutputRow } from "./selectOutputRowEffect.js";
10
+ export { upsertOutputRow } from "./upsertOutputRowEffect.js";
11
+ export { validateOutput } from "./validateOutput.js";
12
+ export { validateExistingOutput } from "./validateExistingOutput.js";
13
+ export { getAgentOutputSchema } from "./getAgentOutputSchema.js";
14
+ export { describeSchemaShape } from "./describeSchemaShape.js";
@@ -0,0 +1,30 @@
1
+ import { Effect } from "effect";
2
+ import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
3
+ import { buildKeyWhere } from "./buildKeyWhere.js";
4
+ /** @typedef {import("./OutputKey.ts").OutputKey} OutputKey */
5
+ /** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
6
+ /** @typedef {import("drizzle-orm").Table} Table */
7
+ /** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
8
+
9
+ /**
10
+ * @template T
11
+ * @param {BunSQLiteDatabase<Record<string, unknown>>} db
12
+ * @param {Table} table
13
+ * @param {OutputKey} key
14
+ * @returns {Effect.Effect<T | undefined, SmithersError>}
15
+ */
16
+ export function selectOutputRow(db, table, key) {
17
+ const where = buildKeyWhere(table, key);
18
+ return Effect.tryPromise({
19
+ try: () => db.select().from(table).where(where).limit(1),
20
+ catch: (cause) => toSmithersError(cause, `select output ${table["_"]?.name ?? "output"}`, {
21
+ code: "DB_QUERY_FAILED",
22
+ details: { outputTable: table["_"]?.name ?? "output" },
23
+ }),
24
+ }).pipe(Effect.map((rows) => rows[0]), Effect.annotateLogs({
25
+ outputTable: table["_"]?.name ?? "output",
26
+ runId: key.runId,
27
+ nodeId: key.nodeId,
28
+ iteration: key.iteration ?? 0,
29
+ }), Effect.withLogSpan("db:select-output-row"));
30
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @param {unknown} payload
3
+ */
4
+ export function stripAutoColumns(payload) {
5
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
6
+ return payload;
7
+ }
8
+ const { runId: _runId, nodeId: _nodeId, iteration: _iteration, ...rest } = payload;
9
+ return rest;
10
+ }
@@ -0,0 +1,38 @@
1
+ import { Effect } from "effect";
2
+ import { toSmithersError } from "@smithers-orchestrator/errors/toSmithersError";
3
+ import { withSqliteWriteRetryEffect } from "../write-retry.js";
4
+ import { getKeyColumns } from "./getKeyColumns.js";
5
+ /** @typedef {import("./OutputKey.ts").OutputKey} OutputKey */
6
+ /** @typedef {import("@smithers-orchestrator/errors/SmithersError").SmithersError} SmithersError */
7
+ /** @typedef {import("drizzle-orm").Table} Table */
8
+ /** @typedef {import("drizzle-orm/bun-sqlite").BunSQLiteDatabase} BunSQLiteDatabase */
9
+
10
+ /**
11
+ * @param {BunSQLiteDatabase<Record<string, unknown>>} db
12
+ * @param {Table} table
13
+ * @param {OutputKey} key
14
+ * @param {Record<string, unknown>} payload
15
+ * @returns {Effect.Effect<void, SmithersError>}
16
+ */
17
+ export function upsertOutputRow(db, table, key, payload) {
18
+ const cols = getKeyColumns(table);
19
+ const values = { ...payload };
20
+ values.runId = key.runId;
21
+ values.nodeId = key.nodeId;
22
+ if (cols.iteration) {
23
+ values.iteration = key.iteration ?? 0;
24
+ }
25
+ const target = cols.iteration ? [cols.runId, cols.nodeId, cols.iteration] : [cols.runId, cols.nodeId];
26
+ return withSqliteWriteRetryEffect(() => Effect.tryPromise({
27
+ try: () => db.insert(table).values(values).onConflictDoUpdate({ target, set: values }),
28
+ catch: (cause) => toSmithersError(cause, `upsert output ${table["_"]?.name ?? "output"}`, {
29
+ code: "DB_WRITE_FAILED",
30
+ details: { outputTable: table["_"]?.name ?? "output" },
31
+ }),
32
+ }), { label: `upsert output ${table["_"]?.name ?? "output"}` }).pipe(Effect.asVoid, Effect.annotateLogs({
33
+ outputTable: table["_"]?.name ?? "output",
34
+ runId: key.runId,
35
+ nodeId: key.nodeId,
36
+ iteration: key.iteration ?? 0,
37
+ }), Effect.withLogSpan("db:upsert-output-row"));
38
+ }
@@ -0,0 +1,17 @@
1
+ import { createSelectSchema } from "drizzle-zod";
2
+ import { z } from "zod";
3
+ /** @typedef {import("drizzle-orm").Table} Table */
4
+
5
+ /**
6
+ * @param {Table} table
7
+ * @param {unknown} payload
8
+ * @returns {{ ok: boolean; data?: any; error?: z.ZodError; }}
9
+ */
10
+ export function validateExistingOutput(table, payload) {
11
+ const schema = createSelectSchema(table);
12
+ const result = schema.safeParse(payload);
13
+ if (result.success) {
14
+ return { ok: true, data: result.data };
15
+ }
16
+ return { ok: false, error: result.error };
17
+ }
@@ -0,0 +1,17 @@
1
+ import { createInsertSchema } from "drizzle-zod";
2
+ import { z } from "zod";
3
+ /** @typedef {import("drizzle-orm").Table} Table */
4
+
5
+ /**
6
+ * @param {Table} table
7
+ * @param {unknown} payload
8
+ * @returns {{ ok: boolean; data?: any; error?: z.ZodError; }}
9
+ */
10
+ export function validateOutput(table, payload) {
11
+ const schema = createInsertSchema(table);
12
+ const result = schema.safeParse(payload);
13
+ if (result.success) {
14
+ return { ok: true, data: result.data };
15
+ }
16
+ return { ok: false, error: result.error };
17
+ }
@@ -0,0 +1,163 @@
1
+ /** @typedef {"string" | "number" | "boolean" | "object" | "array" | "null" | "unknown"} OutputSchemaFieldType */
2
+ /** @typedef {{ name: string; type: OutputSchemaFieldType; optional: boolean; nullable: boolean; description?: string; enum?: readonly unknown[]; }} OutputSchemaFieldDescriptor */
3
+ /** @typedef {{ fields: OutputSchemaFieldDescriptor[]; }} OutputSchemaDescriptor */
4
+ /** @typedef {{ code: "SchemaConversionError"; field: string; construct: string; message: string; }} OutputSchemaDescriptorWarning */
5
+
6
+ /**
7
+ * @param {{ shape?: Record<string, unknown> } & Record<string, unknown>} schema
8
+ * @param {{ onWarning?: (warning: OutputSchemaDescriptorWarning) => void }} [options]
9
+ * @returns {OutputSchemaDescriptor}
10
+ */
11
+ export function buildOutputSchemaDescriptor(schema, options = {}) {
12
+ const shape = schema?.shape;
13
+ if (!shape || typeof shape !== "object" || Array.isArray(shape)) {
14
+ throw new Error("Output schema descriptor requires a Zod object schema.");
15
+ }
16
+ /** @type {OutputSchemaFieldDescriptor[]} */
17
+ const fields = [];
18
+ for (const [name, fieldSchema] of Object.entries(shape)) {
19
+ fields.push(describeField(name, fieldSchema, options));
20
+ }
21
+ return { fields };
22
+ }
23
+ /**
24
+ * @param {string} name
25
+ * @param {unknown} schema
26
+ * @param {{ onWarning?: (warning: OutputSchemaDescriptorWarning) => void }} options
27
+ * @returns {OutputSchemaFieldDescriptor}
28
+ */
29
+ function describeField(name, schema, options) {
30
+ const unwrapped = unwrapSchema(schema);
31
+ const schemaRecord = /** @type {{ description?: unknown } | null | undefined} */ (schema && typeof schema === "object" ? schema : null);
32
+ const innerRecord = /** @type {{ description?: unknown } | null | undefined} */ (unwrapped.schema && typeof unwrapped.schema === "object" ? unwrapped.schema : null);
33
+ const description = asOptionalString(schemaRecord?.description) ?? asOptionalString(innerRecord?.description);
34
+ const base = describeUnwrappedType(name, unwrapped.schema, options);
35
+ /** @type {OutputSchemaFieldDescriptor} */
36
+ const field = {
37
+ name,
38
+ type: base.type,
39
+ optional: unwrapped.optional,
40
+ nullable: unwrapped.nullable,
41
+ };
42
+ if (description) field.description = description;
43
+ if (base.description) field.description = field.description ?? base.description;
44
+ if (base.enumValues) field.enum = base.enumValues;
45
+ return field;
46
+ }
47
+ /**
48
+ * @param {unknown} schema
49
+ * @returns {{ schema: unknown; optional: boolean; nullable: boolean }}
50
+ */
51
+ function unwrapSchema(schema) {
52
+ /** @type {unknown} */
53
+ let current = schema;
54
+ let optional = false;
55
+ let nullable = false;
56
+ let guard = 0;
57
+ /**
58
+ * @param {unknown} value
59
+ * @returns {Record<string, unknown> | undefined}
60
+ */
61
+ const getDef = (value) => {
62
+ if (!value || typeof value !== "object") return undefined;
63
+ const inner = /** @type {{ _zod?: { def?: Record<string, unknown> } }} */ (value);
64
+ return inner._zod?.def;
65
+ };
66
+ let def = getDef(current);
67
+ while (def && guard < 32) {
68
+ guard += 1;
69
+ switch (def?.type) {
70
+ case "optional":
71
+ optional = true;
72
+ current = def.innerType;
73
+ break;
74
+ case "default":
75
+ optional = true;
76
+ current = def.innerType;
77
+ break;
78
+ case "nullable":
79
+ nullable = true;
80
+ current = def.innerType;
81
+ break;
82
+ case "readonly":
83
+ case "nonoptional":
84
+ case "catch":
85
+ current = def.innerType;
86
+ break;
87
+ case "pipe":
88
+ current = def.out ?? def.in ?? current;
89
+ break;
90
+ default:
91
+ return { schema: current, optional, nullable };
92
+ }
93
+ def = getDef(current);
94
+ }
95
+ return { schema: current, optional, nullable };
96
+ }
97
+ /**
98
+ * @param {string} field
99
+ * @param {unknown} schema
100
+ * @param {{ onWarning?: (warning: OutputSchemaDescriptorWarning) => void }} options
101
+ * @returns {{ type: OutputSchemaFieldType; enumValues?: readonly unknown[]; description?: string; }}
102
+ */
103
+ function describeUnwrappedType(field, schema, options) {
104
+ const def = /** @type {Record<string, unknown> | undefined} */ (schema && typeof schema === "object" ? /** @type {{ _zod?: { def?: Record<string, unknown> } }} */ (schema)._zod?.def : undefined);
105
+ const typeName = asOptionalString(def?.type) ?? "unknown";
106
+ switch (typeName) {
107
+ case "string": return { type: "string" };
108
+ case "number":
109
+ case "int":
110
+ case "float": return { type: "number" };
111
+ case "boolean": return { type: "boolean" };
112
+ case "null": return { type: "null" };
113
+ case "array":
114
+ case "tuple":
115
+ case "set": return { type: "array" };
116
+ case "object": return { type: "object" };
117
+ case "record": return { type: "object", description: "Record value shape is not expanded in v1." };
118
+ case "map": return { type: "object", description: "Map entries are not expanded in v1." };
119
+ case "enum": {
120
+ const entries = def?.entries;
121
+ const enumValues = entries && typeof entries === "object" ? Object.values(entries) : [];
122
+ return { type: "string", ...(enumValues.length > 0 ? { enumValues } : {}) };
123
+ }
124
+ case "literal": {
125
+ const values = Array.isArray(def?.values) ? def.values : [];
126
+ if (values.length === 1 && values[0] === null) return { type: "null" };
127
+ if (values.length === 1 && typeof values[0] === "string") return { type: "string", enumValues: values };
128
+ if (values.length === 1 && typeof values[0] === "number") return { type: "number", enumValues: values };
129
+ if (values.length === 1 && typeof values[0] === "boolean") return { type: "boolean", enumValues: values };
130
+ reportUnsupported(field, typeName, options);
131
+ return { type: "unknown" };
132
+ }
133
+ case "unknown":
134
+ case "any":
135
+ case "void":
136
+ case "undefined":
137
+ case "never": return { type: "unknown" };
138
+ default:
139
+ reportUnsupported(field, typeName, options);
140
+ return { type: "unknown" };
141
+ }
142
+ }
143
+ /**
144
+ * @param {string} field
145
+ * @param {string} construct
146
+ * @param {{ onWarning?: (warning: OutputSchemaDescriptorWarning) => void }} options
147
+ * @returns {void}
148
+ */
149
+ function reportUnsupported(field, construct, options) {
150
+ options.onWarning?.({
151
+ code: "SchemaConversionError",
152
+ field,
153
+ construct,
154
+ message: `Unsupported schema construct "${construct}" in output field "${field}".`,
155
+ });
156
+ }
157
+ /**
158
+ * @param {unknown} value
159
+ * @returns {string | undefined}
160
+ */
161
+ function asOptionalString(value) {
162
+ return typeof value === "string" && value.length > 0 ? value : undefined;
163
+ }