@sixb/sqlite 0.1.1 → 0.1.2

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 (108) hide show
  1. package/README.md +18 -13
  2. package/dist/action-run-storage.d.ts +5 -2
  3. package/dist/action-run-storage.d.ts.map +1 -1
  4. package/dist/agents/agent-storage.d.ts +4 -2
  5. package/dist/agents/agent-storage.d.ts.map +1 -1
  6. package/dist/agents/rows.d.ts +3 -10
  7. package/dist/agents/rows.d.ts.map +1 -1
  8. package/dist/agents/runs.d.ts +3 -2
  9. package/dist/agents/runs.d.ts.map +1 -1
  10. package/dist/ai-usage-storage.d.ts +22 -0
  11. package/dist/ai-usage-storage.d.ts.map +1 -0
  12. package/dist/auth-storage/service-accounts.d.ts.map +1 -1
  13. package/dist/auth-storage/shared.d.ts +1 -10
  14. package/dist/auth-storage/shared.d.ts.map +1 -1
  15. package/dist/connector-connection-storage.d.ts +9 -0
  16. package/dist/connector-connection-storage.d.ts.map +1 -0
  17. package/dist/execution-storage.d.ts +14 -0
  18. package/dist/execution-storage.d.ts.map +1 -0
  19. package/dist/index.d.ts +13 -9
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +3862 -1042
  22. package/dist/index.js.map +30 -27
  23. package/dist/latest-run-query.d.ts.map +1 -1
  24. package/dist/migrations.d.ts +0 -1
  25. package/dist/migrations.d.ts.map +1 -1
  26. package/dist/ontology-storage/materialization-session.d.ts +50 -8
  27. package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
  28. package/dist/ontology-storage/materialization-state.d.ts +4 -5
  29. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  30. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  31. package/dist/ontology-storage/materializations.d.ts +0 -2
  32. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  33. package/dist/ontology-storage/outbox.d.ts.map +1 -1
  34. package/dist/ontology-storage/shared.d.ts +3 -3
  35. package/dist/ontology-storage/shared.d.ts.map +1 -1
  36. package/dist/ontology-storage/sources.d.ts.map +1 -1
  37. package/dist/pipeline-run-storage.d.ts +7 -2
  38. package/dist/pipeline-run-storage.d.ts.map +1 -1
  39. package/dist/projection-run-storage.d.ts +9 -2
  40. package/dist/projection-run-storage.d.ts.map +1 -1
  41. package/dist/run-list-query.d.ts +1 -1
  42. package/dist/run-list-query.d.ts.map +1 -1
  43. package/dist/sync-run-storage.d.ts +7 -2
  44. package/dist/sync-run-storage.d.ts.map +1 -1
  45. package/dist/testing.d.ts +6 -0
  46. package/dist/testing.d.ts.map +1 -0
  47. package/dist/transactions.d.ts +3 -0
  48. package/dist/transactions.d.ts.map +1 -1
  49. package/dist/webhook-run-storage.d.ts +9 -2
  50. package/dist/webhook-run-storage.d.ts.map +1 -1
  51. package/dist/workflow-run-storage.d.ts +7 -3
  52. package/dist/workflow-run-storage.d.ts.map +1 -1
  53. package/package.json +3 -3
  54. package/src/action-run-storage.ts +51 -74
  55. package/src/agents/agent-storage.ts +5 -3
  56. package/src/agents/rows.ts +8 -40
  57. package/src/agents/runs.ts +27 -21
  58. package/src/ai-usage-storage.ts +324 -0
  59. package/src/auth-storage/service-accounts.ts +14 -21
  60. package/src/auth-storage/shared.ts +0 -22
  61. package/src/connector-connection-storage.ts +784 -0
  62. package/src/execution-storage.ts +158 -0
  63. package/src/index.ts +63 -22
  64. package/src/latest-run-query.ts +7 -1
  65. package/src/migrations/004-executions.sql +142 -0
  66. package/src/migrations/005-workflow-executions.sql +7 -0
  67. package/src/migrations/006-narrow-ontology-source-root-index.sql +4 -0
  68. package/src/migrations/007-split-overrides.sql +154 -0
  69. package/src/migrations/008-action-executions.sql +4 -0
  70. package/src/migrations/009-agent-executions.sql +115 -0
  71. package/src/migrations/010-ai-usage-accounting-foundation.sql +74 -0
  72. package/src/migrations/011-sync-failure-record.sql +22 -0
  73. package/src/migrations/012-pipeline-failure-record.sql +45 -0
  74. package/src/migrations/013-workflow-failure-record.sql +45 -0
  75. package/src/migrations/014-agent-failure-record.sql +59 -0
  76. package/src/migrations/015-projection-failure-record.sql +20 -0
  77. package/src/migrations/016-webhook-run-failure-record.sql +19 -0
  78. package/src/migrations/017-action-failure-record.sql +65 -0
  79. package/src/migrations/018-ontology-outbox-failure-record.sql +80 -0
  80. package/src/migrations/019-webhook-delivery-failure-record.sql +62 -0
  81. package/src/migrations/020-drop-run-usage-projections.sql +7 -0
  82. package/src/migrations/021-sync-pipeline-executions.sql +101 -0
  83. package/src/migrations/022-projection-executions.sql +244 -0
  84. package/src/migrations/023-webhook-executions.sql +51 -0
  85. package/src/migrations/024-ontology-commit-executions.sql +58 -0
  86. package/src/migrations/025-connector-connections.sql +257 -0
  87. package/src/migrations.ts +119 -4
  88. package/src/ontology-storage/materialization-session.ts +317 -143
  89. package/src/ontology-storage/materialization-state.ts +321 -112
  90. package/src/ontology-storage/materialization-writer.ts +142 -67
  91. package/src/ontology-storage/materializations.ts +67 -141
  92. package/src/ontology-storage/outbox.ts +10 -3
  93. package/src/ontology-storage/shared.ts +10 -4
  94. package/src/ontology-storage/sources.ts +4 -1
  95. package/src/pipeline-run-storage.ts +128 -43
  96. package/src/projection-run-storage.ts +209 -97
  97. package/src/run-list-query.ts +9 -4
  98. package/src/sync-run-storage.ts +118 -32
  99. package/src/testing.ts +20 -0
  100. package/src/transactions.ts +10 -3
  101. package/src/webhook-run-storage.ts +118 -74
  102. package/src/workflow-run-storage.ts +117 -131
  103. package/dist/validation.d.ts +0 -5
  104. package/dist/validation.d.ts.map +0 -1
  105. package/dist/webhook-delivery-storage.d.ts +0 -27
  106. package/dist/webhook-delivery-storage.d.ts.map +0 -1
  107. package/src/validation.ts +0 -30
  108. package/src/webhook-delivery-storage.ts +0 -272
@@ -0,0 +1,324 @@
1
+ import type { ReadonlyJsonObject } from "@sixb/core/storage"
2
+ import {
3
+ type AiModelCallUsageInput,
4
+ type AiModelCallUsageRecord,
5
+ type AiUsageExecutionSummary,
6
+ type AiUsageStorage,
7
+ AiUsageStorageError,
8
+ aggregateAiModelCallUsage,
9
+ assertAiUsageExecutionId,
10
+ normalizeAiModelCallRecord,
11
+ type RecordAiModelCallInput,
12
+ type RecordAiModelCallResult,
13
+ type SummarizeAiUsageExecutionInput,
14
+ type SummarizeAiUsageExecutionsInput,
15
+ } from "@sixb/core/storage"
16
+ import { installFreshSqliteSchema } from "./migrations"
17
+ import { isUniqueConstraintError } from "./storage-errors"
18
+ import {
19
+ closeSqliteStoreConnection,
20
+ openSqliteStoreConnection,
21
+ runImmediateTransaction,
22
+ type SqliteStoreConnection,
23
+ } from "./transactions"
24
+
25
+ export interface SqliteAiUsageStorageOptions {
26
+ readonly path?: string
27
+ readonly connection?: SqliteStoreConnection
28
+ }
29
+
30
+ /** SQLite-backed append-only model-call usage ledger. */
31
+ export class SqliteAiUsageStorage implements AiUsageStorage {
32
+ private readonly connection: SqliteStoreConnection
33
+
34
+ constructor(options: SqliteAiUsageStorageOptions = {}) {
35
+ this.connection = openSqliteStoreConnection(options)
36
+ if (this.connection.installFreshSchema) {
37
+ installFreshSqliteSchema(this.connection.db)
38
+ }
39
+ }
40
+
41
+ async recordModelCall(input: RecordAiModelCallInput): Promise<RecordAiModelCallResult> {
42
+ const record = normalizeAiModelCallRecord(input)
43
+ return runImmediateTransaction(this.connection.db, () => {
44
+ const existing = this.findByIdempotencyKey(record)
45
+ if (existing) return { record: existing, created: false }
46
+ this.assertExecutionExists(record.projectId, record.executionId)
47
+
48
+ try {
49
+ this.insertRecord(record)
50
+ } catch (error) {
51
+ if (!isUniqueConstraintError(error)) throw error
52
+
53
+ const concurrent = this.findByIdempotencyKey(record)
54
+ if (concurrent) return { record: concurrent, created: false }
55
+ throw new AiUsageStorageError(
56
+ "duplicate_id",
57
+ `[SixbSqlite] AI usage record '${record.id}' already exists for project '${record.projectId}'.`
58
+ )
59
+ }
60
+
61
+ for (const groupId of record.requesterGroupIds) {
62
+ this.connection.db
63
+ .query(
64
+ `
65
+ INSERT INTO ai_model_call_usage_groups (
66
+ project_id, usage_record_id, group_id, occurred_at
67
+ ) VALUES (?, ?, ?, ?)
68
+ `
69
+ )
70
+ .run(record.projectId, record.id, groupId, record.occurredAt.toISOString())
71
+ }
72
+
73
+ return { record: this.requireById(record.projectId, record.id), created: true }
74
+ })
75
+ }
76
+
77
+ async summarizeExecution(
78
+ input: SummarizeAiUsageExecutionInput
79
+ ): Promise<AiUsageExecutionSummary> {
80
+ const [summary] = await this.summarizeExecutions({
81
+ projectId: input.projectId,
82
+ executionIds: [input.executionId],
83
+ })
84
+ return summary!
85
+ }
86
+
87
+ async summarizeExecutions(
88
+ input: SummarizeAiUsageExecutionsInput
89
+ ): Promise<readonly AiUsageExecutionSummary[]> {
90
+ assertNonBlankProjectId(input.projectId)
91
+ for (const executionId of input.executionIds) assertAiUsageExecutionId(executionId)
92
+ if (input.executionIds.length === 0) return []
93
+
94
+ const rows = this.executionRows(input)
95
+ return aggregateExecutionRows(input.executionIds, rows)
96
+ }
97
+
98
+ close(): void {
99
+ closeSqliteStoreConnection(this.connection)
100
+ }
101
+
102
+ private insertRecord(record: AiModelCallUsageRecord): void {
103
+ this.connection.db
104
+ .query(
105
+ `
106
+ INSERT INTO ai_model_call_usage (
107
+ project_id,
108
+ id,
109
+ execution_id,
110
+ attempt,
111
+ call_id,
112
+ provider_id,
113
+ requested_model_id,
114
+ response_model_id,
115
+ response_id,
116
+ input_tokens,
117
+ output_tokens,
118
+ total_tokens,
119
+ uncached_input_tokens,
120
+ cache_read_input_tokens,
121
+ cache_write_input_tokens,
122
+ text_output_tokens,
123
+ reasoning_output_tokens,
124
+ reporting_status,
125
+ raw_usage,
126
+ occurred_at,
127
+ recorded_at
128
+ ) VALUES (
129
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
130
+ )
131
+ `
132
+ )
133
+ .run(
134
+ record.projectId,
135
+ record.id,
136
+ record.executionId,
137
+ record.attempt,
138
+ record.callId,
139
+ record.providerId,
140
+ record.requestedModelId,
141
+ record.responseModelId ?? null,
142
+ record.responseId,
143
+ record.usage.inputTokens ?? null,
144
+ record.usage.outputTokens ?? null,
145
+ record.usage.totalTokens ?? null,
146
+ record.usage.uncachedInputTokens ?? null,
147
+ record.usage.cacheReadInputTokens ?? null,
148
+ record.usage.cacheWriteInputTokens ?? null,
149
+ record.usage.textOutputTokens ?? null,
150
+ record.usage.reasoningOutputTokens ?? null,
151
+ record.usage.reportingStatus,
152
+ record.rawUsage === undefined ? null : JSON.stringify(record.rawUsage),
153
+ record.occurredAt.toISOString(),
154
+ record.recordedAt.toISOString()
155
+ )
156
+ }
157
+
158
+ private findByIdempotencyKey(
159
+ record: Pick<
160
+ AiModelCallUsageRecord,
161
+ "projectId" | "executionId" | "attempt" | "callId" | "responseId"
162
+ >
163
+ ): AiModelCallUsageRecord | null {
164
+ const row = this.connection.db
165
+ .query(
166
+ `
167
+ SELECT * FROM ai_model_call_usage
168
+ WHERE project_id = ?
169
+ AND execution_id = ?
170
+ AND attempt = ?
171
+ AND call_id = ?
172
+ AND response_id = ?
173
+ `
174
+ )
175
+ .get(
176
+ record.projectId,
177
+ record.executionId,
178
+ record.attempt,
179
+ record.callId,
180
+ record.responseId
181
+ ) as AiUsageRow | null
182
+ return row ? this.rowToRecord(row) : null
183
+ }
184
+
185
+ private requireById(projectId: string, id: string): AiModelCallUsageRecord {
186
+ const row = this.connection.db
187
+ .query("SELECT * FROM ai_model_call_usage WHERE project_id = ? AND id = ?")
188
+ .get(projectId, id) as AiUsageRow | null
189
+ if (!row) throw new Error(`[SixbSqlite] AI usage record '${id}' disappeared after insert.`)
190
+ return this.rowToRecord(row)
191
+ }
192
+
193
+ private rowToRecord(row: AiUsageRow): AiModelCallUsageRecord {
194
+ const groupRows = this.connection.db
195
+ .query(
196
+ `
197
+ SELECT group_id FROM ai_model_call_usage_groups
198
+ WHERE project_id = ? AND usage_record_id = ?
199
+ ORDER BY group_id
200
+ `
201
+ )
202
+ .all(row.project_id, row.id) as Array<{ group_id: string }>
203
+
204
+ return {
205
+ id: row.id,
206
+ projectId: row.project_id,
207
+ executionId: row.execution_id,
208
+ attempt: row.attempt,
209
+ callId: row.call_id,
210
+ requesterGroupIds: groupRows.map((group) => group.group_id),
211
+ providerId: row.provider_id,
212
+ requestedModelId: row.requested_model_id,
213
+ ...(row.response_model_id === null ? {} : { responseModelId: row.response_model_id }),
214
+ responseId: row.response_id,
215
+ usage: {
216
+ ...usageFromRow(row),
217
+ ...(row.total_tokens === null ? {} : { totalTokens: row.total_tokens }),
218
+ reportingStatus: row.reporting_status,
219
+ },
220
+ ...(row.raw_usage === null
221
+ ? {}
222
+ : { rawUsage: JSON.parse(row.raw_usage) as ReadonlyJsonObject }),
223
+ occurredAt: new Date(row.occurred_at),
224
+ recordedAt: new Date(row.recorded_at),
225
+ }
226
+ }
227
+
228
+ private executionRows(input: SummarizeAiUsageExecutionsInput): AiUsageRow[] {
229
+ const requested = JSON.stringify([...new Set(input.executionIds)])
230
+ return this.connection.db
231
+ .query(
232
+ `
233
+ WITH requested AS (
234
+ SELECT DISTINCT value AS execution_id
235
+ FROM json_each(?)
236
+ )
237
+ SELECT usage.* FROM requested
238
+ JOIN ai_model_call_usage AS usage
239
+ ON usage.project_id = ?
240
+ AND usage.execution_id = requested.execution_id
241
+ `
242
+ )
243
+ .all(requested, input.projectId) as AiUsageRow[]
244
+ }
245
+
246
+ private assertExecutionExists(projectId: string, executionId: string): void {
247
+ const execution = this.connection.db
248
+ .query("SELECT 1 FROM executions WHERE project_id = ? AND id = ?")
249
+ .get(projectId, executionId)
250
+ if (execution) return
251
+ throw new AiUsageStorageError(
252
+ "missing_execution",
253
+ `[SixbSqlite] AI usage execution '${executionId}' does not exist in project '${projectId}'.`
254
+ )
255
+ }
256
+ }
257
+
258
+ interface AiUsageRow {
259
+ readonly project_id: string
260
+ readonly id: string
261
+ readonly execution_id: string
262
+ readonly attempt: number
263
+ readonly call_id: string
264
+ readonly provider_id: string
265
+ readonly requested_model_id: string
266
+ readonly response_model_id: string | null
267
+ readonly response_id: string
268
+ readonly input_tokens: number | null
269
+ readonly output_tokens: number | null
270
+ readonly total_tokens: number | null
271
+ readonly uncached_input_tokens: number | null
272
+ readonly cache_read_input_tokens: number | null
273
+ readonly cache_write_input_tokens: number | null
274
+ readonly text_output_tokens: number | null
275
+ readonly reasoning_output_tokens: number | null
276
+ readonly reporting_status: AiModelCallUsageRecord["usage"]["reportingStatus"]
277
+ readonly raw_usage: string | null
278
+ readonly occurred_at: string
279
+ readonly recorded_at: string
280
+ }
281
+
282
+ function aggregateExecutionRows(
283
+ executionIds: readonly string[],
284
+ rows: readonly AiUsageRow[]
285
+ ): readonly AiUsageExecutionSummary[] {
286
+ const usageByExecutionId = new Map<string, AiModelCallUsageInput[]>()
287
+ for (const executionId of executionIds) usageByExecutionId.set(executionId, [])
288
+ for (const row of rows) {
289
+ usageByExecutionId.get(row.execution_id)?.push(usageFromRow(row))
290
+ }
291
+ return executionIds.map((executionId) => {
292
+ const usages = usageByExecutionId.get(executionId) ?? []
293
+ return {
294
+ modelCallCount: usages.length,
295
+ usage: aggregateAiModelCallUsage(usages),
296
+ }
297
+ })
298
+ }
299
+
300
+ function usageFromRow(row: AiUsageRow): AiModelCallUsageInput {
301
+ return {
302
+ ...(row.input_tokens === null ? {} : { inputTokens: row.input_tokens }),
303
+ ...(row.output_tokens === null ? {} : { outputTokens: row.output_tokens }),
304
+ ...(row.uncached_input_tokens === null
305
+ ? {}
306
+ : { uncachedInputTokens: row.uncached_input_tokens }),
307
+ ...(row.cache_read_input_tokens === null
308
+ ? {}
309
+ : { cacheReadInputTokens: row.cache_read_input_tokens }),
310
+ ...(row.cache_write_input_tokens === null
311
+ ? {}
312
+ : { cacheWriteInputTokens: row.cache_write_input_tokens }),
313
+ ...(row.text_output_tokens === null ? {} : { textOutputTokens: row.text_output_tokens }),
314
+ ...(row.reasoning_output_tokens === null
315
+ ? {}
316
+ : { reasoningOutputTokens: row.reasoning_output_tokens }),
317
+ }
318
+ }
319
+
320
+ function assertNonBlankProjectId(projectId: string): void {
321
+ if (typeof projectId !== "string" || projectId.trim().length === 0) {
322
+ throw new TypeError("[Sixb] AI usage projectId must be nonblank.")
323
+ }
324
+ }
@@ -92,40 +92,33 @@ export class SqliteAuthServiceAccountStore implements AuthServiceAccountStore {
92
92
  }
93
93
 
94
94
  async update(input: UpdateAuthServiceAccountInput): Promise<ServiceAccountRecord> {
95
- const existing = await this.getById(input)
96
- if (!existing) {
97
- throw new AuthStorageError(
98
- "missing_service_account",
99
- `[Sixb] Service account '${input.id}' not found for project '${input.projectId}'.`
100
- )
101
- }
102
-
95
+ const id = assertNonEmpty(input.id, "Service account id")
96
+ const projectId = assertNonEmpty(input.projectId, "Project id")
103
97
  const updatedAt = dateOrNow(input.updatedAt)
104
98
  const name =
105
- input.name === undefined ? existing.name : assertNonEmpty(input.name, "Service account name")
106
- const description = input.description === undefined ? existing.description : input.description
107
- const status = input.status ?? existing.status
99
+ input.name === undefined ? null : assertNonEmpty(input.name, "Service account name")
108
100
  this.db
109
101
  .query(
110
102
  `
111
103
  UPDATE auth_service_accounts
112
- SET name = ?,
113
- description = ?,
114
- status = ?,
104
+ SET name = COALESCE(?, name),
105
+ description = COALESCE(?, description),
106
+ status = COALESCE(?, status),
115
107
  updated_at = ?
116
108
  WHERE project_id = ?
117
109
  AND id = ?
118
110
  `
119
111
  )
120
- .run(name, description ?? null, status, toIso(updatedAt), input.projectId, input.id)
112
+ .run(name, input.description ?? null, input.status ?? null, toIso(updatedAt), projectId, id)
121
113
 
122
- return {
123
- ...existing,
124
- name,
125
- description,
126
- status,
127
- updatedAt,
114
+ const updated = await this.getById({ projectId, id })
115
+ if (!updated) {
116
+ throw new AuthStorageError(
117
+ "missing_service_account",
118
+ `[Sixb] Service account '${id}' not found for project '${projectId}'.`
119
+ )
128
120
  }
121
+ return updated
129
122
  }
130
123
 
131
124
  async list(input: ListAuthServiceAccountsInput): Promise<ListAuthServiceAccountsResult> {
@@ -8,7 +8,6 @@ import type {
8
8
  MagicLinkRecord,
9
9
  OidcAuthorizationAttemptRecord,
10
10
  UpsertAuthGroupMembershipInput,
11
- UserRecord,
12
11
  } from "@sixb/core/storage"
13
12
  import { AuthStorageError } from "@sixb/core/storage"
14
13
  import { isUniqueConstraintError } from "../storage-errors"
@@ -27,7 +26,6 @@ import {
27
26
  rowToMagicLinkRecord,
28
27
  rowToOidcAuthorizationAttemptRecord,
29
28
  rowToSessionRecord,
30
- rowToUserRecord,
31
29
  } from "./rows"
32
30
 
33
31
  export type SqliteValue = string | number | null
@@ -48,10 +46,6 @@ export function normalizeGroupIds(groupIds: readonly string[] | undefined): read
48
46
  return [...new Set((groupIds ?? []).map((groupId) => assertNonEmpty(groupId, "Group id")))]
49
47
  }
50
48
 
51
- export function cloneRecord<T>(record: T): T {
52
- return structuredClone(record)
53
- }
54
-
55
49
  export function dateOrNow(value: Date | undefined): Date {
56
50
  return value ? new Date(value) : new Date()
57
51
  }
@@ -93,14 +87,6 @@ export function getUserRowById(
93
87
  .get(params.projectId, params.id) as SqliteAuthUserRow | null
94
88
  }
95
89
 
96
- export function getUserById(
97
- db: Database,
98
- params: { readonly projectId: string; readonly id: string }
99
- ): UserRecord | null {
100
- const row = getUserRowById(db, params)
101
- return row ? rowToUserRecord(row) : null
102
- }
103
-
104
90
  export function getUserRowByEmail(
105
91
  db: Database,
106
92
  params: { readonly projectId: string; readonly email: string }
@@ -110,14 +96,6 @@ export function getUserRowByEmail(
110
96
  .get(params.projectId, normalizeEmail(params.email)) as SqliteAuthUserRow | null
111
97
  }
112
98
 
113
- export function getUserByEmail(
114
- db: Database,
115
- params: { readonly projectId: string; readonly email: string }
116
- ): UserRecord | null {
117
- const row = getUserRowByEmail(db, params)
118
- return row ? rowToUserRecord(row) : null
119
- }
120
-
121
99
  export function getIdentityRowBySubject(
122
100
  db: Database,
123
101
  params: {