| @@ -1,982 +0,0 @@ |
| 1 | import { createHash, randomUUID } from "node:crypto"; | | |
| 2 | import { mkdirSync } from "node:fs"; | | |
| 3 | import { dirname, resolve as resolvePath } from "node:path"; | | |
| 4 | | | |
| 5 | import { Database } from "bun:sqlite"; | | |
| 6 | import { | | |
| 7 | createExecutor, | | |
| 8 | createExecutorBackend, | | |
| 9 | type CreateExecutorOptions, | | |
| 10 | type Executor, | | |
| 11 | type ExecutorBackend, | | |
| 12 | type ExecutorBackendServices, | | |
| 13 | type ExecutorStateBackend, | | |
| 14 | type ExecutorInstallationBackend, | | |
| 15 | type ExecutorLocalToolBackend, | | |
| 16 | type ExecutorSecretMaterialBackend, | | |
| 17 | type ExecutorSourceArtifactBackend, | | |
| 18 | type ExecutorSourceTypeDeclarationsBackend, | | |
| 19 | type ExecutorScopeConfigBackend, | | |
| 20 | type ExecutorScopeStateBackend, | | |
| 21 | } from "@executor/platform-sdk"; | | |
| 22 | import type { ExecutorRuntimeOptions } from "@executor/platform-sdk/runtime"; | | |
| 23 | import { | | |
| 24 | type AuthArtifact, | | |
| 25 | ScopeIdSchema, | | |
| 26 | AuthArtifactSchema, | | |
| 27 | type AuthLease, | | |
| 28 | AuthLeaseSchema, | | |
| 29 | type Execution, | | |
| 30 | type ExecutionInteraction, | | |
| 31 | ExecutionInteractionSchema, | | |
| 32 | type ExecutionStep, | | |
| 33 | ExecutionSchema, | | |
| 34 | ExecutionStepSchema, | | |
| 35 | type LocalInstallation, | | |
| 36 | LocalExecutorConfigSchema, | | |
| 37 | LocalInstallationSchema, | | |
| 38 | type ProviderAuthGrant, | | |
| 39 | ProviderAuthGrantSchema, | | |
| 40 | SecretMaterialIdSchema, | | |
| 41 | SecretMaterialSchema, | | |
| 42 | type SourceAuthSession, | | |
| 43 | SourceAuthSessionSchema, | | |
| 44 | SourceCatalogIdSchema, | | |
| 45 | SourceCatalogRevisionIdSchema, | | |
| 46 | type ScopeOauthClient, | | |
| 47 | ScopeOauthClientSchema, | | |
| 48 | type ScopedSourceOauthClient, | | |
| 49 | ScopedSourceOauthClientSchema, | | |
| 50 | type SecretMaterial, | | |
| 51 | type SecretRef, | | |
| 52 | } from "@executor/platform-sdk/schema"; | | |
| 53 | import * as Effect from "effect/Effect"; | | |
| 54 | import * as Option from "effect/Option"; | | |
| 55 | import * as Schema from "effect/Schema"; | | |
| 56 | | | |
| 57 | import { contentHash, snapshotFromSourceCatalogSyncResult } from "@executor/source-core"; | | |
| 58 | | | |
| 59 | export type CreateSqliteExecutorBackendOptions = { | | |
| 60 | databasePath?: string; | | |
| 61 | scopeName?: string; | | |
| 62 | scopeRoot?: string | null; | | |
| 63 | scopeId?: string; | | |
| 64 | actorScopeId?: string; | | |
| 65 | }; | | |
| 66 | | | |
| 67 | type DocumentNamespace = | | |
| 68 | | "installation" | | |
| 69 | | "scope_config" | | |
| 70 | | "scope_state" | | |
| 71 | | "source_artifact" | | |
| 72 | | "executor_state"; | | |
| 73 | | | |
| 74 | type ExecutorStateCollections = { | | |
| 75 | authArtifacts: readonly AuthArtifact[]; | | |
| 76 | authLeases: readonly AuthLease[]; | | |
| 77 | sourceOauthClients: readonly ScopedSourceOauthClient[]; | | |
| 78 | scopeOauthClients: readonly ScopeOauthClient[]; | | |
| 79 | providerAuthGrants: readonly ProviderAuthGrant[]; | | |
| 80 | sourceAuthSessions: readonly SourceAuthSession[]; | | |
| 81 | secretMaterials: readonly SecretMaterial[]; | | |
| 82 | executions: readonly Execution[]; | | |
| 83 | executionInteractions: readonly ExecutionInteraction[]; | | |
| 84 | executionSteps: readonly ExecutionStep[]; | | |
| 85 | }; | | |
| 86 | | | |
| 87 | const decodeInstallation = Schema.decodeUnknownSync(LocalInstallationSchema); | | |
| 88 | const decodeConfig = Schema.decodeUnknownSync(LocalExecutorConfigSchema); | | |
| 89 | const decodeAuthArtifacts = Schema.decodeUnknownSync(Schema.Array(AuthArtifactSchema)); | | |
| 90 | const decodeAuthLeases = Schema.decodeUnknownSync(Schema.Array(AuthLeaseSchema)); | | |
| 91 | const decodeSourceOauthClients = Schema.decodeUnknownSync( | | |
| 92 | Schema.Array(ScopedSourceOauthClientSchema), | | |
| 93 | ); | | |
| 94 | const decodeScopeOauthClients = Schema.decodeUnknownSync( | | |
| 95 | Schema.Array(ScopeOauthClientSchema), | | |
| 96 | ); | | |
| 97 | const decodeProviderAuthGrants = Schema.decodeUnknownSync( | | |
| 98 | Schema.Array(ProviderAuthGrantSchema), | | |
| 99 | ); | | |
| 100 | const decodeSourceAuthSessions = Schema.decodeUnknownSync( | | |
| 101 | Schema.Array(SourceAuthSessionSchema), | | |
| 102 | ); | | |
| 103 | const decodeSecretMaterials = Schema.decodeUnknownSync( | | |
| 104 | Schema.Array(SecretMaterialSchema), | | |
| 105 | ); | | |
| 106 | const decodeExecutions = Schema.decodeUnknownSync(Schema.Array(ExecutionSchema)); | | |
| 107 | const decodeExecutionInteractions = Schema.decodeUnknownSync( | | |
| 108 | Schema.Array(ExecutionInteractionSchema), | | |
| 109 | ); | | |
| 110 | const decodeExecutionSteps = Schema.decodeUnknownSync( | | |
| 111 | Schema.Array(ExecutionStepSchema), | | |
| 112 | ); | | |
| 113 | | | |
| 114 | type SourceArtifact = ReturnType<ExecutorSourceArtifactBackend["build"]>; | | |
| 115 | type SourceArtifactBuildInput = Parameters<ExecutorSourceArtifactBackend["build"]>[0]; | | |
| 116 | | | |
| 117 | const SQLITE_SECRET_PROVIDER_ID = "sqlite"; | | |
| 118 | | | |
| 119 | type ScopeState = Parameters<ExecutorScopeStateBackend["write"]>[0]; | | |
| 120 | const defaultProjectConfig = decodeConfig({}); | | |
| 121 | | | |
| 122 | const defaultScopeState = (): ScopeState => ({ | | |
| 123 | version: 1 as const, | | |
| 124 | sources: {}, | | |
| 125 | policies: {}, | | |
| 126 | }); | | |
| 127 | | | |
| 128 | const makeIdHash = (value: string): string => | | |
| 129 | createHash("sha256").update(value).digest("hex").slice(0, 24); | | |
| 130 | | | |
| 131 | const makeSourceArtifact = (input: SourceArtifactBuildInput): SourceArtifact => { | | |
| 132 | const snapshot = snapshotFromSourceCatalogSyncResult(input.syncResult); | | |
| 133 | const sourceSignature = JSON.stringify({ | | |
| 134 | kind: input.source.kind, | | |
| 135 | endpoint: input.source.endpoint, | | |
| 136 | namespace: input.source.namespace, | | |
| 137 | name: input.source.name, | | |
| 138 | enabled: input.source.enabled, | | |
| 139 | binding: input.source.binding, | | |
| 140 | auth: input.source.auth, | | |
| 141 | importAuth: input.source.importAuth, | | |
| 142 | importAuthPolicy: input.source.importAuthPolicy, | | |
| 143 | }); | | |
| 144 | const catalogId = SourceCatalogIdSchema.make( | | |
| 145 | `src_catalog_${makeIdHash(sourceSignature)}`, | | |
| 146 | ); | | |
| 147 | const revisionId = SourceCatalogRevisionIdSchema.make( | | |
| 148 | `src_catalog_rev_${makeIdHash(sourceSignature)}`, | | |
| 149 | ); | | |
| 150 | const importMetadataJson = JSON.stringify(snapshot.import); | | |
| 151 | const snapshotHash = contentHash(JSON.stringify(snapshot)); | | |
| 152 | const importMetadataHash = contentHash(importMetadataJson); | | |
| 153 | | | |
| 154 | return { | | |
| 155 | version: 4 as const, | | |
| 156 | sourceId: input.source.id, | | |
| 157 | catalogId, | | |
| 158 | generatedAt: Date.now(), | | |
| 159 | revision: { | | |
| 160 | id: revisionId, | | |
| 161 | catalogId, | | |
| 162 | revisionNumber: 1, | | |
| 163 | sourceConfigJson: sourceSignature, | | |
| 164 | importMetadataJson, | | |
| 165 | importMetadataHash, | | |
| 166 | snapshotHash, | | |
| 167 | createdAt: input.source.createdAt, | | |
| 168 | updatedAt: input.source.updatedAt, | | |
| 169 | }, | | |
| 170 | snapshot, | | |
| 171 | }; | | |
| 172 | }; | | |
| 173 | | | |
| 174 | const toError = (cause: unknown): Error => | | |
| 175 | cause instanceof Error ? cause : new Error(String(cause)); | | |
| 176 | | | |
| 177 | class SqliteDocumentStore { | | |
| 178 | readonly db: Database; | | |
| 179 | | | |
| 180 | constructor(databasePath: string) { | | |
| 181 | if (databasePath !== ":memory:") { | | |
| 182 | mkdirSync(dirname(databasePath), { recursive: true }); | | |
| 183 | } | | |
| 184 | | | |
| 185 | this.db = new Database(databasePath, { | | |
| 186 | create: true, | | |
| 187 | strict: true, | | |
| 188 | }); | | |
| 189 | | | |
| 190 | this.db.exec(` | | |
| 191 | CREATE TABLE IF NOT EXISTS executor_documents ( | | |
| 192 | namespace TEXT NOT NULL, | | |
| 193 | key TEXT NOT NULL, | | |
| 194 | value TEXT NOT NULL, | | |
| 195 | updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000), | | |
| 196 | PRIMARY KEY (namespace, key) | | |
| 197 | ); | | |
| 198 | `); | | |
| 199 | } | | |
| 200 | | | |
| 201 | close() { | | |
| 202 | this.db.close(); | | |
| 203 | } | | |
| 204 | | | |
| 205 | read<T>( | | |
| 206 | namespace: DocumentNamespace, | | |
| 207 | key: string, | | |
| 208 | decode: (value: unknown) => T, | | |
| 209 | fallback: () => T, | | |
| 210 | ): T { | | |
| 211 | const row = this.db | | |
| 212 | .query( | | |
| 213 | "SELECT value FROM executor_documents WHERE namespace = ?1 AND key = ?2", | | |
| 214 | ) | | |
| 215 | .get(namespace, key) as { value: string } | null; | | |
| 216 | | | |
| 217 | if (!row) { | | |
| 218 | return fallback(); | | |
| 219 | } | | |
| 220 | | | |
| 221 | return decode(JSON.parse(row.value) as unknown); | | |
| 222 | } | | |
| 223 | | | |
| 224 | write(namespace: DocumentNamespace, key: string, value: unknown) { | | |
| 225 | this.db | | |
| 226 | .query(` | | |
| 227 | INSERT INTO executor_documents (namespace, key, value, updated_at) | | |
| 228 | VALUES (?1, ?2, ?3, ?4) | | |
| 229 | ON CONFLICT(namespace, key) DO UPDATE SET | | |
| 230 | value = excluded.value, | | |
| 231 | updated_at = excluded.updated_at | | |
| 232 | `) | | |
| 233 | .run(namespace, key, JSON.stringify(value), Date.now()); | | |
| 234 | } | | |
| 235 | | | |
| 236 | remove(namespace: DocumentNamespace, key: string) { | | |
| 237 | this.db | | |
| 238 | .query("DELETE FROM executor_documents WHERE namespace = ?1 AND key = ?2") | | |
| 239 | .run(namespace, key); | | |
| 240 | } | | |
| 241 | } | | |
| 242 | | | |
| 243 | const effectTry = <T>(label: string, execute: () => T) => | | |
| 244 | Effect.try({ | | |
| 245 | try: execute, | | |
| 246 | catch: (cause) => new Error(`${label}: ${toError(cause).message}`), | | |
| 247 | }); | | |
| 248 | | | |
| 249 | const sameActor = ( | | |
| 250 | left: string | null | undefined, | | |
| 251 | right: string | null | undefined, | | |
| 252 | ): boolean => (left ?? null) === (right ?? null); | | |
| 253 | | | |
| 254 | const createInstallationStore = ( | | |
| 255 | store: SqliteDocumentStore, | | |
| 256 | options: CreateSqliteExecutorBackendOptions, | | |
| 257 | ): ExecutorInstallationBackend => { | | |
| 258 | const provision = (): LocalInstallation => | | |
| 259 | decodeInstallation({ | | |
| 260 | actorScopeId: ScopeIdSchema.make(options.actorScopeId ?? "acct_sqlite_example"), | | |
| 261 | scopeId: ScopeIdSchema.make(options.scopeId ?? "ws_sqlite_example"), | | |
| 262 | resolutionScopeIds: [ | | |
| 263 | ScopeIdSchema.make(options.scopeId ?? "ws_sqlite_example"), | | |
| 264 | ScopeIdSchema.make(options.actorScopeId ?? "acct_sqlite_example"), | | |
| 265 | ], | | |
| 266 | }); | | |
| 267 | | | |
| 268 | return { | | |
| 269 | load: () => | | |
| 270 | effectTry("load installation", () => | | |
| 271 | store.read("installation", "active", decodeInstallation, provision), | | |
| 272 | ), | | |
| 273 | getOrProvision: () => | | |
| 274 | effectTry("get or provision installation", () => { | | |
| 275 | const installation = store.read("installation", "active", decodeInstallation, provision); | | |
| 276 | store.write("installation", "active", installation); | | |
| 277 | return installation; | | |
| 278 | }), | | |
| 279 | }; | | |
| 280 | }; | | |
| 281 | | | |
| 282 | const createScopeConfigStore = ( | | |
| 283 | store: SqliteDocumentStore, | | |
| 284 | ): ExecutorScopeConfigBackend => ({ | | |
| 285 | load: () => | | |
| 286 | effectTry("load workspace config", () => { | | |
| 287 | const projectConfig = store.read( | | |
| 288 | "scope_config", | | |
| 289 | "project", | | |
| 290 | decodeConfig, | | |
| 291 | () => defaultProjectConfig, | | |
| 292 | ); | | |
| 293 | | | |
| 294 | return { | | |
| 295 | config: projectConfig, | | |
| 296 | homeConfig: null, | | |
| 297 | projectConfig, | | |
| 298 | }; | | |
| 299 | }), | | |
| 300 | writeProject: (config) => | | |
| 301 | effectTry("write workspace config", () => { | | |
| 302 | store.write("scope_config", "project", config); | | |
| 303 | }), | | |
| 304 | resolveRelativePath: ({ path, scopeRoot }) => | | |
| 305 | resolvePath(scopeRoot, path), | | |
| 306 | }); | | |
| 307 | | | |
| 308 | const createScopeStateStore = ( | | |
| 309 | store: SqliteDocumentStore, | | |
| 310 | ): ExecutorScopeStateBackend => ({ | | |
| 311 | load: () => | | |
| 312 | effectTry("load workspace state", () => | | |
| 313 | store.read("scope_state", "active", (value) => value as ReturnType<typeof defaultScopeState>, defaultScopeState), | | |
| 314 | ), | | |
| 315 | write: (state) => | | |
| 316 | effectTry("write workspace state", () => { | | |
| 317 | store.write("scope_state", "active", state); | | |
| 318 | }), | | |
| 319 | }); | | |
| 320 | | | |
| 321 | const createSourceArtifactStore = ( | | |
| 322 | store: SqliteDocumentStore, | | |
| 323 | ): ExecutorSourceArtifactBackend => ({ | | |
| 324 | build: makeSourceArtifact, | | |
| 325 | read: (sourceId) => | | |
| 326 | effectTry("read source artifact", () => | | |
| 327 | store.read( | | |
| 328 | "source_artifact", | | |
| 329 | sourceId, | | |
| 330 | (value) => value as SourceArtifact, | | |
| 331 | () => null, | | |
| 332 | ), | | |
| 333 | ), | | |
| 334 | write: ({ sourceId, artifact }) => | | |
| 335 | effectTry("write source artifact", () => { | | |
| 336 | store.write("source_artifact", sourceId, artifact); | | |
| 337 | }), | | |
| 338 | remove: (sourceId) => | | |
| 339 | effectTry("remove source artifact", () => { | | |
| 340 | store.remove("source_artifact", sourceId); | | |
| 341 | }), | | |
| 342 | }); | | |
| 343 | | | |
| 344 | const createExecutorStateStore = ( | | |
| 345 | store: SqliteDocumentStore, | | |
| 346 | ): ExecutorStateBackend => { | | |
| 347 | const readCollection = <K extends keyof ExecutorStateCollections>( | | |
| 348 | key: K, | | |
| 349 | ): ExecutorStateCollections[K] => { | | |
| 350 | switch (key) { | | |
| 351 | case "authArtifacts": | | |
| 352 | return store.read("executor_state", key, decodeAuthArtifacts, () => []) as ExecutorStateCollections[K]; | | |
| 353 | case "authLeases": | | |
| 354 | return store.read("executor_state", key, decodeAuthLeases, () => []) as ExecutorStateCollections[K]; | | |
| 355 | case "sourceOauthClients": | | |
| 356 | return store.read("executor_state", key, decodeSourceOauthClients, () => []) as ExecutorStateCollections[K]; | | |
| 357 | case "scopeOauthClients": | | |
| 358 | return store.read("executor_state", key, decodeScopeOauthClients, () => []) as ExecutorStateCollections[K]; | | |
| 359 | case "providerAuthGrants": | | |
| 360 | return store.read("executor_state", key, decodeProviderAuthGrants, () => []) as ExecutorStateCollections[K]; | | |
| 361 | case "sourceAuthSessions": | | |
| 362 | return store.read("executor_state", key, decodeSourceAuthSessions, () => []) as ExecutorStateCollections[K]; | | |
| 363 | case "secretMaterials": | | |
| 364 | return store.read("executor_state", key, decodeSecretMaterials, () => []) as ExecutorStateCollections[K]; | | |
| 365 | case "executions": | | |
| 366 | return store.read("executor_state", key, decodeExecutions, () => []) as ExecutorStateCollections[K]; | | |
| 367 | case "executionInteractions": | | |
| 368 | return store.read("executor_state", key, decodeExecutionInteractions, () => []) as ExecutorStateCollections[K]; | | |
| 369 | case "executionSteps": | | |
| 370 | return store.read("executor_state", key, decodeExecutionSteps, () => []) as ExecutorStateCollections[K]; | | |
| 371 | } | | |
| 372 | }; | | |
| 373 | | | |
| 374 | const writeCollection = <K extends keyof ExecutorStateCollections>( | | |
| 375 | key: K, | | |
| 376 | values: ExecutorStateCollections[K], | | |
| 377 | ) => { | | |
| 378 | store.write("executor_state", key, values); | | |
| 379 | }; | | |
| 380 | | | |
| 381 | const updateCollection = <K extends keyof ExecutorStateCollections, Result>( | | |
| 382 | key: K, | | |
| 383 | mutate: (values: Array<ExecutorStateCollections[K][number]>) => Result, | | |
| 384 | ): Result => { | | |
| 385 | const nextValues = [...readCollection(key)] as Array<ExecutorStateCollections[K][number]>; | | |
| 386 | const result = mutate(nextValues); | | |
| 387 | writeCollection(key, nextValues as ExecutorStateCollections[K]); | | |
| 388 | return result; | | |
| 389 | }; | | |
| 390 | | | |
| 391 | return { | | |
| 392 | authArtifacts: { | | |
| 393 | listByScopeId: (scopeId) => | | |
| 394 | effectTry("list auth artifacts by workspace", () => | | |
| 395 | readCollection("authArtifacts").filter((item) => item.scopeId === scopeId), | | |
| 396 | ), | | |
| 397 | listByScopeAndSourceId: ({ scopeId, sourceId }) => | | |
| 398 | effectTry("list auth artifacts by source", () => | | |
| 399 | readCollection("authArtifacts").filter((item) => | | |
| 400 | item.scopeId === scopeId && item.sourceId === sourceId | | |
| 401 | ), | | |
| 402 | ), | | |
| 403 | getByScopeSourceAndActor: ({ scopeId, sourceId, actorScopeId, slot }) => | | |
| 404 | effectTry("get auth artifact", () => | | |
| 405 | Option.fromNullable( | | |
| 406 | readCollection("authArtifacts").find((item) => | | |
| 407 | item.scopeId === scopeId | | |
| 408 | && item.sourceId === sourceId | | |
| 409 | && sameActor(item.actorScopeId, actorScopeId) | | |
| 410 | && item.slot === slot | | |
| 411 | ), | | |
| 412 | ), | | |
| 413 | ), | | |
| 414 | upsert: (artifact) => | | |
| 415 | effectTry("upsert auth artifact", () => { | | |
| 416 | updateCollection("authArtifacts", (items) => { | | |
| 417 | const index = items.findIndex((item) => | | |
| 418 | item.scopeId === artifact.scopeId | | |
| 419 | && item.sourceId === artifact.sourceId | | |
| 420 | && sameActor(item.actorScopeId, artifact.actorScopeId) | | |
| 421 | && item.slot === artifact.slot | | |
| 422 | ); | | |
| 423 | if (index >= 0) { | | |
| 424 | items[index] = artifact; | | |
| 425 | } else { | | |
| 426 | items.push(artifact); | | |
| 427 | } | | |
| 428 | }); | | |
| 429 | }), | | |
| 430 | removeByScopeSourceAndActor: ({ scopeId, sourceId, actorScopeId, slot }) => | | |
| 431 | effectTry("remove auth artifact", () => | | |
| 432 | updateCollection("authArtifacts", (items) => { | | |
| 433 | const before = items.length; | | |
| 434 | const remaining = items.filter((item) => !( | | |
| 435 | item.scopeId === scopeId | | |
| 436 | && item.sourceId === sourceId | | |
| 437 | && sameActor(item.actorScopeId, actorScopeId) | | |
| 438 | && (slot === undefined || item.slot === slot) | | |
| 439 | )); | | |
| 440 | items.splice(0, items.length, ...remaining); | | |
| 441 | return before !== remaining.length; | | |
| 442 | }), | | |
| 443 | ), | | |
| 444 | removeByScopeAndSourceId: ({ scopeId, sourceId }) => | | |
| 445 | effectTry("remove auth artifacts by source", () => | | |
| 446 | updateCollection("authArtifacts", (items) => { | | |
| 447 | const remaining = items.filter((item) => | | |
| 448 | !(item.scopeId === scopeId && item.sourceId === sourceId) | | |
| 449 | ); | | |
| 450 | const removed = items.length - remaining.length; | | |
| 451 | items.splice(0, items.length, ...remaining); | | |
| 452 | return removed; | | |
| 453 | }), | | |
| 454 | ), | | |
| 455 | }, | | |
| 456 | authLeases: { | | |
| 457 | listAll: () => | | |
| 458 | effectTry("list auth leases", () => readCollection("authLeases")), | | |
| 459 | getByAuthArtifactId: (authArtifactId) => | | |
| 460 | effectTry("get auth lease", () => | | |
| 461 | Option.fromNullable( | | |
| 462 | readCollection("authLeases").find((item) => item.authArtifactId === authArtifactId), | | |
| 463 | ), | | |
| 464 | ), | | |
| 465 | upsert: (lease) => | | |
| 466 | effectTry("upsert auth lease", () => { | | |
| 467 | updateCollection("authLeases", (items) => { | | |
| 468 | const index = items.findIndex((item) => item.authArtifactId === lease.authArtifactId); | | |
| 469 | if (index >= 0) { | | |
| 470 | items[index] = lease; | | |
| 471 | } else { | | |
| 472 | items.push(lease); | | |
| 473 | } | | |
| 474 | }); | | |
| 475 | }), | | |
| 476 | removeByAuthArtifactId: (authArtifactId) => | | |
| 477 | effectTry("remove auth lease", () => | | |
| 478 | updateCollection("authLeases", (items) => { | | |
| 479 | const remaining = items.filter((item) => item.authArtifactId !== authArtifactId); | | |
| 480 | const removed = remaining.length !== items.length; | | |
| 481 | items.splice(0, items.length, ...remaining); | | |
| 482 | return removed; | | |
| 483 | }), | | |
| 484 | ), | | |
| 485 | }, | | |
| 486 | sourceOauthClients: { | | |
| 487 | getByScopeSourceAndProvider: ({ scopeId, sourceId, providerKey }) => | | |
| 488 | effectTry("get source oauth client", () => | | |
| 489 | Option.fromNullable( | | |
| 490 | readCollection("sourceOauthClients").find((item) => | | |
| 491 | item.scopeId === scopeId | | |
| 492 | && item.sourceId === sourceId | | |
| 493 | && item.providerKey === providerKey | | |
| 494 | ), | | |
| 495 | ), | | |
| 496 | ), | | |
| 497 | upsert: (oauthClient) => | | |
| 498 | effectTry("upsert source oauth client", () => { | | |
| 499 | updateCollection("sourceOauthClients", (items) => { | | |
| 500 | const index = items.findIndex((item) => | | |
| 501 | item.scopeId === oauthClient.scopeId | | |
| 502 | && item.sourceId === oauthClient.sourceId | | |
| 503 | && item.providerKey === oauthClient.providerKey | | |
| 504 | ); | | |
| 505 | if (index >= 0) { | | |
| 506 | items[index] = oauthClient; | | |
| 507 | } else { | | |
| 508 | items.push(oauthClient); | | |
| 509 | } | | |
| 510 | }); | | |
| 511 | }), | | |
| 512 | removeByScopeAndSourceId: ({ scopeId, sourceId }) => | | |
| 513 | effectTry("remove source oauth clients", () => | | |
| 514 | updateCollection("sourceOauthClients", (items) => { | | |
| 515 | const remaining = items.filter((item) => | | |
| 516 | !(item.scopeId === scopeId && item.sourceId === sourceId) | | |
| 517 | ); | | |
| 518 | const removed = items.length - remaining.length; | | |
| 519 | items.splice(0, items.length, ...remaining); | | |
| 520 | return removed; | | |
| 521 | }), | | |
| 522 | ), | | |
| 523 | }, | | |
| 524 | scopeOauthClients: { | | |
| 525 | listByScopeAndProvider: ({ scopeId, providerKey }) => | | |
| 526 | effectTry("list workspace oauth clients", () => | | |
| 527 | readCollection("scopeOauthClients").filter((item) => | | |
| 528 | item.scopeId === scopeId && item.providerKey === providerKey | | |
| 529 | ), | | |
| 530 | ), | | |
| 531 | getById: (id) => | | |
| 532 | effectTry("get workspace oauth client", () => | | |
| 533 | Option.fromNullable( | | |
| 534 | readCollection("scopeOauthClients").find((item) => item.id === id), | | |
| 535 | ), | | |
| 536 | ), | | |
| 537 | upsert: (oauthClient) => | | |
| 538 | effectTry("upsert workspace oauth client", () => { | | |
| 539 | updateCollection("scopeOauthClients", (items) => { | | |
| 540 | const index = items.findIndex((item) => item.id === oauthClient.id); | | |
| 541 | if (index >= 0) { | | |
| 542 | items[index] = oauthClient; | | |
| 543 | } else { | | |
| 544 | items.push(oauthClient); | | |
| 545 | } | | |
| 546 | }); | | |
| 547 | }), | | |
| 548 | removeById: (id) => | | |
| 549 | effectTry("remove workspace oauth client", () => | | |
| 550 | updateCollection("scopeOauthClients", (items) => { | | |
| 551 | const remaining = items.filter((item) => item.id !== id); | | |
| 552 | const removed = remaining.length !== items.length; | | |
| 553 | items.splice(0, items.length, ...remaining); | | |
| 554 | return removed; | | |
| 555 | }), | | |
| 556 | ), | | |
| 557 | }, | | |
| 558 | providerAuthGrants: { | | |
| 559 | listByScopeId: (scopeId) => | | |
| 560 | effectTry("list provider grants by workspace", () => | | |
| 561 | readCollection("providerAuthGrants").filter((item) => item.scopeId === scopeId), | | |
| 562 | ), | | |
| 563 | listByScopeActorAndProvider: ({ scopeId, actorScopeId, providerKey }) => | | |
| 564 | effectTry("list provider grants by actor", () => | | |
| 565 | readCollection("providerAuthGrants").filter((item) => | | |
| 566 | item.scopeId === scopeId | | |
| 567 | && item.actorScopeId === actorScopeId | | |
| 568 | && item.providerKey === providerKey | | |
| 569 | ), | | |
| 570 | ), | | |
| 571 | getById: (id) => | | |
| 572 | effectTry("get provider grant", () => | | |
| 573 | Option.fromNullable( | | |
| 574 | readCollection("providerAuthGrants").find((item) => item.id === id), | | |
| 575 | ), | | |
| 576 | ), | | |
| 577 | upsert: (grant) => | | |
| 578 | effectTry("upsert provider grant", () => { | | |
| 579 | updateCollection("providerAuthGrants", (items) => { | | |
| 580 | const index = items.findIndex((item) => item.id === grant.id); | | |
| 581 | if (index >= 0) { | | |
| 582 | items[index] = grant; | | |
| 583 | } else { | | |
| 584 | items.push(grant); | | |
| 585 | } | | |
| 586 | }); | | |
| 587 | }), | | |
| 588 | removeById: (id) => | | |
| 589 | effectTry("remove provider grant", () => | | |
| 590 | updateCollection("providerAuthGrants", (items) => { | | |
| 591 | const remaining = items.filter((item) => item.id !== id); | | |
| 592 | const removed = remaining.length !== items.length; | | |
| 593 | items.splice(0, items.length, ...remaining); | | |
| 594 | return removed; | | |
| 595 | }), | | |
| 596 | ), | | |
| 597 | }, | | |
| 598 | sourceAuthSessions: { | | |
| 599 | listAll: () => | | |
| 600 | effectTry("list source auth sessions", () => readCollection("sourceAuthSessions")), | | |
| 601 | listByScopeId: (scopeId) => | | |
| 602 | effectTry("list source auth sessions by workspace", () => | | |
| 603 | readCollection("sourceAuthSessions").filter((item) => item.scopeId === scopeId), | | |
| 604 | ), | | |
| 605 | getById: (id) => | | |
| 606 | effectTry("get source auth session", () => | | |
| 607 | Option.fromNullable( | | |
| 608 | readCollection("sourceAuthSessions").find((item) => item.id === id), | | |
| 609 | ), | | |
| 610 | ), | | |
| 611 | getByState: (state) => | | |
| 612 | effectTry("get source auth session by state", () => | | |
| 613 | Option.fromNullable( | | |
| 614 | readCollection("sourceAuthSessions").find((item) => item.state === state), | | |
| 615 | ), | | |
| 616 | ), | | |
| 617 | getPendingByScopeSourceAndActor: ({ scopeId, sourceId, actorScopeId, credentialSlot }) => | | |
| 618 | effectTry("get pending source auth session", () => | | |
| 619 | Option.fromNullable( | | |
| 620 | readCollection("sourceAuthSessions").find((item) => | | |
| 621 | item.scopeId === scopeId | | |
| 622 | && item.sourceId === sourceId | | |
| 623 | && item.actorScopeId === actorScopeId | | |
| 624 | && item.status === "pending" | | |
| 625 | && (credentialSlot === undefined || item.credentialSlot === credentialSlot) | | |
| 626 | ), | | |
| 627 | ), | | |
| 628 | ), | | |
| 629 | insert: (session) => | | |
| 630 | effectTry("insert source auth session", () => { | | |
| 631 | updateCollection("sourceAuthSessions", (items) => { | | |
| 632 | items.push(session); | | |
| 633 | }); | | |
| 634 | }), | | |
| 635 | update: (id, patch) => | | |
| 636 | effectTry("update source auth session", () => | | |
| 637 | updateCollection("sourceAuthSessions", (items) => { | | |
| 638 | const index = items.findIndex((item) => item.id === id); | | |
| 639 | if (index < 0) { | | |
| 640 | return Option.none(); | | |
| 641 | } | | |
| 642 | | | |
| 643 | const next = { | | |
| 644 | ...items[index], | | |
| 645 | ...patch, | | |
| 646 | }; | | |
| 647 | items[index] = next; | | |
| 648 | return Option.some(next); | | |
| 649 | }), | | |
| 650 | ), | | |
| 651 | upsert: (session) => | | |
| 652 | effectTry("upsert source auth session", () => { | | |
| 653 | updateCollection("sourceAuthSessions", (items) => { | | |
| 654 | const index = items.findIndex((item) => item.id === session.id); | | |
| 655 | if (index >= 0) { | | |
| 656 | items[index] = session; | | |
| 657 | } else { | | |
| 658 | items.push(session); | | |
| 659 | } | | |
| 660 | }); | | |
| 661 | }), | | |
| 662 | removeByScopeAndSourceId: (scopeId, sourceId) => | | |
| 663 | effectTry("remove source auth sessions", () => | | |
| 664 | updateCollection("sourceAuthSessions", (items) => { | | |
| 665 | const remaining = items.filter((item) => | | |
| 666 | !(item.scopeId === scopeId && item.sourceId === sourceId) | | |
| 667 | ); | | |
| 668 | const removed = remaining.length !== items.length; | | |
| 669 | items.splice(0, items.length, ...remaining); | | |
| 670 | return removed; | | |
| 671 | }), | | |
| 672 | ), | | |
| 673 | }, | | |
| 674 | secretMaterials: { | | |
| 675 | getById: (id) => | | |
| 676 | effectTry("get secret material", () => | | |
| 677 | Option.fromNullable( | | |
| 678 | readCollection("secretMaterials").find((item) => item.id === id), | | |
| 679 | ), | | |
| 680 | ), | | |
| 681 | listAll: () => | | |
| 682 | effectTry("list secret materials", () => | | |
| 683 | readCollection("secretMaterials").map((item) => ({ | | |
| 684 | id: item.id, | | |
| 685 | providerId: item.providerId, | | |
| 686 | name: item.name, | | |
| 687 | purpose: item.purpose, | | |
| 688 | createdAt: item.createdAt, | | |
| 689 | updatedAt: item.updatedAt, | | |
| 690 | })), | | |
| 691 | ), | | |
| 692 | upsert: (material) => | | |
| 693 | effectTry("upsert secret material", () => { | | |
| 694 | updateCollection("secretMaterials", (items) => { | | |
| 695 | const index = items.findIndex((item) => item.id === material.id); | | |
| 696 | if (index >= 0) { | | |
| 697 | items[index] = material; | | |
| 698 | } else { | | |
| 699 | items.push(material); | | |
| 700 | } | | |
| 701 | }); | | |
| 702 | }), | | |
| 703 | updateById: (id, update) => | | |
| 704 | effectTry("update secret material", () => | | |
| 705 | updateCollection("secretMaterials", (items) => { | | |
| 706 | const index = items.findIndex((item) => item.id === id); | | |
| 707 | if (index < 0) { | | |
| 708 | return Option.none(); | | |
| 709 | } | | |
| 710 | | | |
| 711 | const next = { | | |
| 712 | ...items[index], | | |
| 713 | name: update.name === undefined ? items[index]!.name : update.name, | | |
| 714 | value: update.value === undefined ? items[index]!.value : update.value, | | |
| 715 | updatedAt: Date.now(), | | |
| 716 | }; | | |
| 717 | items[index] = next; | | |
| 718 | return Option.some({ | | |
| 719 | id: next.id, | | |
| 720 | providerId: next.providerId, | | |
| 721 | name: next.name, | | |
| 722 | purpose: next.purpose, | | |
| 723 | createdAt: next.createdAt, | | |
| 724 | updatedAt: next.updatedAt, | | |
| 725 | }); | | |
| 726 | }), | | |
| 727 | ), | | |
| 728 | removeById: (id) => | | |
| 729 | effectTry("remove secret material", () => | | |
| 730 | updateCollection("secretMaterials", (items) => { | | |
| 731 | const remaining = items.filter((item) => item.id !== id); | | |
| 732 | const removed = remaining.length !== items.length; | | |
| 733 | items.splice(0, items.length, ...remaining); | | |
| 734 | return removed; | | |
| 735 | }), | | |
| 736 | ), | | |
| 737 | }, | | |
| 738 | executions: { | | |
| 739 | getById: (executionId) => | | |
| 740 | effectTry("get execution by id", () => | | |
| 741 | Option.fromNullable( | | |
| 742 | readCollection("executions").find((item) => item.id === executionId), | | |
| 743 | ), | | |
| 744 | ), | | |
| 745 | getByScopeAndId: (scopeId, executionId) => | | |
| 746 | effectTry("get execution by scope and id", () => | | |
| 747 | Option.fromNullable( | | |
| 748 | readCollection("executions").find((item) => | | |
| 749 | item.scopeId === scopeId && item.id === executionId | | |
| 750 | ), | | |
| 751 | ), | | |
| 752 | ), | | |
| 753 | insert: (execution) => | | |
| 754 | effectTry("insert execution", () => { | | |
| 755 | updateCollection("executions", (items) => { | | |
| 756 | items.push(execution); | | |
| 757 | }); | | |
| 758 | }), | | |
| 759 | update: (executionId, patch) => | | |
| 760 | effectTry("update execution", () => | | |
| 761 | updateCollection("executions", (items) => { | | |
| 762 | const index = items.findIndex((item) => item.id === executionId); | | |
| 763 | if (index < 0) { | | |
| 764 | return Option.none(); | | |
| 765 | } | | |
| 766 | | | |
| 767 | const next = { | | |
| 768 | ...items[index], | | |
| 769 | ...patch, | | |
| 770 | }; | | |
| 771 | items[index] = next; | | |
| 772 | return Option.some(next); | | |
| 773 | }), | | |
| 774 | ), | | |
| 775 | }, | | |
| 776 | executionInteractions: { | | |
| 777 | getById: (interactionId) => | | |
| 778 | effectTry("get execution interaction", () => | | |
| 779 | Option.fromNullable( | | |
| 780 | readCollection("executionInteractions").find((item) => item.id === interactionId), | | |
| 781 | ), | | |
| 782 | ), | | |
| 783 | listByExecutionId: (executionId) => | | |
| 784 | effectTry("list execution interactions", () => | | |
| 785 | readCollection("executionInteractions").filter((item) => item.executionId === executionId), | | |
| 786 | ), | | |
| 787 | getPendingByExecutionId: (executionId) => | | |
| 788 | effectTry("get pending execution interaction", () => | | |
| 789 | Option.fromNullable( | | |
| 790 | readCollection("executionInteractions").find((item) => | | |
| 791 | item.executionId === executionId && item.status === "pending" | | |
| 792 | ), | | |
| 793 | ), | | |
| 794 | ), | | |
| 795 | insert: (interaction) => | | |
| 796 | effectTry("insert execution interaction", () => { | | |
| 797 | updateCollection("executionInteractions", (items) => { | | |
| 798 | items.push(interaction); | | |
| 799 | }); | | |
| 800 | }), | | |
| 801 | update: (interactionId, patch) => | | |
| 802 | effectTry("update execution interaction", () => | | |
| 803 | updateCollection("executionInteractions", (items) => { | | |
| 804 | const index = items.findIndex((item) => item.id === interactionId); | | |
| 805 | if (index < 0) { | | |
| 806 | return Option.none(); | | |
| 807 | } | | |
| 808 | | | |
| 809 | const next = { | | |
| 810 | ...items[index], | | |
| 811 | ...patch, | | |
| 812 | }; | | |
| 813 | items[index] = next; | | |
| 814 | return Option.some(next); | | |
| 815 | }), | | |
| 816 | ), | | |
| 817 | }, | | |
| 818 | executionSteps: { | | |
| 819 | getByExecutionAndSequence: (executionId, sequence) => | | |
| 820 | effectTry("get execution step", () => | | |
| 821 | Option.fromNullable( | | |
| 822 | readCollection("executionSteps").find((item) => | | |
| 823 | item.executionId === executionId && item.sequence === sequence | | |
| 824 | ), | | |
| 825 | ), | | |
| 826 | ), | | |
| 827 | listByExecutionId: (executionId) => | | |
| 828 | effectTry("list execution steps", () => | | |
| 829 | readCollection("executionSteps") | | |
| 830 | .filter((item) => item.executionId === executionId) | | |
| 831 | .sort((left, right) => left.sequence - right.sequence), | | |
| 832 | ), | | |
| 833 | insert: (step) => | | |
| 834 | effectTry("insert execution step", () => { | | |
| 835 | updateCollection("executionSteps", (items) => { | | |
| 836 | items.push(step); | | |
| 837 | }); | | |
| 838 | }), | | |
| 839 | deleteByExecutionId: (executionId) => | | |
| 840 | effectTry("delete execution steps", () => { | | |
| 841 | updateCollection("executionSteps", (items) => { | | |
| 842 | const remaining = items.filter((item) => item.executionId !== executionId); | | |
| 843 | items.splice(0, items.length, ...remaining); | | |
| 844 | }); | | |
| 845 | }), | | |
| 846 | updateByExecutionAndSequence: (executionId, sequence, patch) => | | |
| 847 | effectTry("update execution step", () => | | |
| 848 | updateCollection("executionSteps", (items) => { | | |
| 849 | const index = items.findIndex((item) => | | |
| 850 | item.executionId === executionId && item.sequence === sequence | | |
| 851 | ); | | |
| 852 | if (index < 0) { | | |
| 853 | return Option.none(); | | |
| 854 | } | | |
| 855 | | | |
| 856 | const next = { | | |
| 857 | ...items[index], | | |
| 858 | ...patch, | | |
| 859 | }; | | |
| 860 | items[index] = next; | | |
| 861 | return Option.some(next); | | |
| 862 | }), | | |
| 863 | ), | | |
| 864 | }, | | |
| 865 | }; | | |
| 866 | }; | | |
| 867 | | | |
| 868 | const createSecretMaterialBackend = ( | | |
| 869 | executorState: ExecutorStateBackend, | | |
| 870 | ): ExecutorSecretMaterialBackend => { | | |
| 871 | const getSecretMaterial = (id: string) => | | |
| 872 | Effect.flatMap(executorState.secretMaterials.getById(id as SecretMaterial["id"]), (result) => | | |
| 873 | Option.isSome(result) | | |
| 874 | ? Effect.succeed(result.value) | | |
| 875 | : Effect.fail(new Error(`Secret material ${id} not found`)), | | |
| 876 | ); | | |
| 877 | | | |
| 878 | return { | | |
| 879 | resolve: ({ ref }) => | | |
| 880 | Effect.flatMap(getSecretMaterial(ref.handle), (material) => { | | |
| 881 | if (material.value === null) { | | |
| 882 | return Effect.fail(new Error(`Secret material ${material.id} has no stored value`)); | | |
| 883 | } | | |
| 884 | | | |
| 885 | return Effect.succeed(material.value); | | |
| 886 | }), | | |
| 887 | store: ({ purpose, value, name, providerId }) => | | |
| 888 | Effect.gen(function* () { | | |
| 889 | const now = Date.now(); | | |
| 890 | const id = SecretMaterialIdSchema.make(`secret_${randomUUID()}`); | | |
| 891 | const material: SecretMaterial = { | | |
| 892 | id, | | |
| 893 | providerId: providerId ?? SQLITE_SECRET_PROVIDER_ID, | | |
| 894 | handle: id, | | |
| 895 | name: name ?? null, | | |
| 896 | purpose, | | |
| 897 | value, | | |
| 898 | createdAt: now, | | |
| 899 | updatedAt: now, | | |
| 900 | }; | | |
| 901 | yield* executorState.secretMaterials.upsert(material); | | |
| 902 | return { | | |
| 903 | providerId: material.providerId, | | |
| 904 | handle: material.handle, | | |
| 905 | } satisfies SecretRef; | | |
| 906 | }), | | |
| 907 | delete: (ref) => | | |
| 908 | executorState.secretMaterials.removeById(ref.handle as SecretMaterial["id"]), | | |
| 909 | update: ({ ref, name, value }) => | | |
| 910 | Effect.flatMap( | | |
| 911 | executorState.secretMaterials.updateById(ref.handle as SecretMaterial["id"], { | | |
| 912 | name, | | |
| 913 | value, | | |
| 914 | }), | | |
| 915 | (result) => | | |
| 916 | Option.isSome(result) | | |
| 917 | ? Effect.succeed(result.value) | | |
| 918 | : Effect.fail(new Error(`Secret material ${ref.handle} not found`)), | | |
| 919 | ), | | |
| 920 | }; | | |
| 921 | }; | | |
| 922 | | | |
| 923 | export const createSqliteExecutorBackend = ( | | |
| 924 | options: CreateSqliteExecutorBackendOptions = {}, | | |
| 925 | ): ExecutorBackend => { | | |
| 926 | const databasePath = options.databasePath === undefined || options.databasePath === ":memory:" | | |
| 927 | ? options.databasePath ?? ":memory:" | | |
| 928 | : resolvePath(options.databasePath); | | |
| 929 | | | |
| 930 | return createExecutorBackend({ | | |
| 931 | loadServices: () => | | |
| 932 | Effect.sync(() => { | | |
| 933 | const store = new SqliteDocumentStore(databasePath); | | |
| 934 | const executorState = createExecutorStateStore(store); | | |
| 935 | const secretMaterial = createSecretMaterialBackend(executorState); | | |
| 936 | | | |
| 937 | return { | | |
| 938 | scope: { | | |
| 939 | scopeName: options.scopeName ?? "SQLite SDK Example", | | |
| 940 | scopeRoot: options.scopeRoot ?? null, | | |
| 941 | metadata: { | | |
| 942 | kind: "sqlite", | | |
| 943 | databasePath, | | |
| 944 | }, | | |
| 945 | }, | | |
| 946 | storage: { | | |
| 947 | installation: createInstallationStore(store, options), | | |
| 948 | scopeConfig: createScopeConfigStore(store), | | |
| 949 | scopeState: createScopeStateStore(store), | | |
| 950 | sourceArtifacts: createSourceArtifactStore(store), | | |
| 951 | executorState, | | |
| 952 | secretMaterial, | | |
| 953 | close: async () => { | | |
| 954 | store.close(); | | |
| 955 | }, | | |
| 956 | }, | | |
| 957 | instanceConfig: { | | |
| 958 | resolve: () => | | |
| 959 | Effect.succeed({ | | |
| 960 | platform: "sqlite-sdk-example", | | |
| 961 | secretProviders: [ | | |
| 962 | { | | |
| 963 | id: SQLITE_SECRET_PROVIDER_ID, | | |
| 964 | name: "SQLite", | | |
| 965 | canStore: true, | | |
| 966 | }, | | |
| 967 | ], | | |
| 968 | defaultSecretStoreProvider: SQLITE_SECRET_PROVIDER_ID, | | |
| 969 | }), | | |
| 970 | }, | | |
| 971 | } satisfies ExecutorBackendServices; | | |
| 972 | }), | | |
| 973 | }); | | |
| 974 | }; | | |
| 975 | | | |
| 976 | export const createSqliteExecutor = ( | | |
| 977 | options: CreateSqliteExecutorBackendOptions & ExecutorRuntimeOptions = {}, | | |
| 978 | ): Promise<Executor> => | | |
| 979 | createExecutor({ | | |
| 980 | ...options, | | |
| 981 | backend: createSqliteExecutorBackend(options), | | |
| 982 | } satisfies CreateExecutorOptions); | | |