Unified · Split
| @@ -5,11 +5,12 @@ · 4 unmodified lines | ||
| 5 | 5 | // openapi_source_binding.value), runs the migration runner, asserts |
| 6 | 6 | // the new flat columns + child tables match. |
| 7 | 7 | |
| 8 | −import { describe, expect, it } from "@effect/vitest"; | |
| 8 | +import { afterEach, beforeEach, describe, expect, it } from "@effect/vitest"; | |
| 9 | 9 | import { Database } from "bun:sqlite"; |
| 10 | 10 | import { mkdtempSync, rmSync } from "node:fs"; |
| 11 | 11 | import { join } from "node:path"; |
| 12 | 12 | import { tmpdir } from "node:os"; |
| 13 | +import { Schema } from "effect"; | |
| 13 | 14 | import { drizzle } from "drizzle-orm/bun-sqlite"; |
| 14 | 15 | import { migrate } from "drizzle-orm/bun-sqlite/migrator"; |
| 15 | 16 | |
| @@ -17,235 +18,276 @@ import { PRE_0007_SQL, stampPriorMigrationsApplied } from "./__test-helpers__/pr · 1 unmodified line | ||
| 17 | 18 | |
| 18 | 19 | const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle"); |
| 19 | 20 | |
| 21 | +const BindingRow = Schema.Struct({ | |
| 22 | + id: Schema.String, | |
| 23 | + kind: Schema.String, | |
| 24 | + secret_id: Schema.NullOr(Schema.String), | |
| 25 | + connection_id: Schema.NullOr(Schema.String), | |
| 26 | + text_value: Schema.NullOr(Schema.String), | |
| 27 | +}); | |
| 28 | + | |
| 29 | +const QueryParamRow = Schema.Struct({ | |
| 30 | + name: Schema.String, | |
| 31 | + kind: Schema.String, | |
| 32 | + text_value: Schema.NullOr(Schema.String), | |
| 33 | + secret_id: Schema.NullOr(Schema.String), | |
| 34 | +}); | |
| 35 | + | |
| 36 | +const FetchHeaderRow = Schema.Struct({ | |
| 37 | + name: Schema.String, | |
| 38 | + kind: Schema.String, | |
| 39 | + secret_id: Schema.NullOr(Schema.String), | |
| 40 | + secret_prefix: Schema.NullOr(Schema.String), | |
| 41 | +}); | |
| 42 | + | |
| 43 | +const FetchQueryParamRow = Schema.Struct({ | |
| 44 | + name: Schema.String, | |
| 45 | + secret_id: Schema.String, | |
| 46 | +}); | |
| 47 | + | |
| 48 | +const TableInfoRow = Schema.Struct({ | |
| 49 | + name: Schema.String, | |
| 50 | +}); | |
| 51 | + | |
| 52 | +const CountRow = Schema.Struct({ | |
| 53 | + n: Schema.Number, | |
| 54 | +}); | |
| 55 | + | |
| 20 | 56 | describe("0007_normalize_plugin_secret_refs (openapi)", () => { |
| 21 | − it("flattens openapi_source_binding.value into kind/secret_id/connection_id/text_value", () => { | |
| 22 | − const dir = mkdtempSync(join(tmpdir(), "openapi-mig-")); | |
| 23 | − const dbPath = join(dir, "test.sqlite"); | |
| 24 | − try { | |
| 25 | − const db = new Database(dbPath); | |
| 26 | − db.exec(PRE_0007_SQL); | |
| 27 | − stampPriorMigrationsApplied(db); | |
| 28 | − | |
| 29 | − // Seed three bindings, one per kind. | |
| 30 | − const insert = db.prepare( | |
| 31 | − "INSERT INTO openapi_source_binding (id, source_id, source_scope_id, target_scope_id, slot, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", | |
| 32 | − ); | |
| 33 | − const now = Date.now(); | |
| 34 | − insert.run( | |
| 35 | − "b1", | |
| 36 | − "src", | |
| 37 | − "default-scope", | |
| 38 | − "default-scope", | |
| 39 | − "header:authorization", | |
| 40 | − JSON.stringify({ kind: "secret", secretId: "tok-secret" }), | |
| 41 | − now, | |
| 42 | − now, | |
| 43 | − ); | |
| 44 | − insert.run( | |
| 45 | − "b2", | |
| 46 | − "src", | |
| 47 | − "default-scope", | |
| 48 | − "default-scope", | |
| 49 | − "oauth2:default:connection", | |
| 50 | − JSON.stringify({ kind: "connection", connectionId: "conn-1" }), | |
| 51 | − now, | |
| 52 | − now, | |
| 53 | − ); | |
| 54 | − insert.run( | |
| 55 | − "b3", | |
| 56 | − "src", | |
| 57 | − "default-scope", | |
| 58 | − "default-scope", | |
| 59 | − "header:x-static", | |
| 60 | − JSON.stringify({ kind: "text", text: "literal" }), | |
| 61 | − now, | |
| 62 | − now, | |
| 63 | − ); | |
| 64 | − | |
| 65 | − // Need the parent openapi_source row so the source_id FK ergonomics | |
| 66 | − // are satisfied for any cascading delete logic — though the binding | |
| 67 | − // table has no DB-level FK, code paths assume the parent exists. | |
| 68 | − db.prepare( | |
| 69 | − "INSERT INTO openapi_source (scope_id, id, name, spec, invocation_config) VALUES (?, ?, ?, ?, ?)", | |
| 70 | − ).run("default-scope", "src", "Source", "{}", "{}"); | |
| 57 | + let dir: string; | |
| 58 | + let dbPath: string; | |
| 59 | + let openDatabases: Set<Database>; | |
| 60 | + | |
| 61 | + beforeEach(() => { | |
| 62 | + dir = mkdtempSync(join(tmpdir(), "openapi-mig-")); | |
| 63 | + dbPath = join(dir, "test.sqlite"); | |
| 64 | + openDatabases = new Set(); | |
| 65 | + }); | |
| 71 | 66 | |
| 67 | + afterEach(() => { | |
| 68 | + for (const db of openDatabases) { | |
| 72 | 69 | db.close(); |
| 70 | + } | |
| 71 | + openDatabases.clear(); | |
| 72 | + rmSync(dir, { recursive: true, force: true }); | |
| 73 | + }); | |
| 74 | + | |
| 75 | + const openDatabase = (...args: ConstructorParameters<typeof Database>) => { | |
| 76 | + const db = new Database(...args); | |
| 77 | + openDatabases.add(db); | |
| 78 | + return db; | |
| 79 | + }; | |
| 80 | + | |
| 81 | + const closeDatabase = (db: Database) => { | |
| 82 | + db.close(); | |
| 83 | + openDatabases.delete(db); | |
| 84 | + }; | |
| 73 | 85 | |
| 74 | − const drizzleDb = drizzle(new Database(dbPath)); | |
| 75 | − migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | |
| 86 | + it("flattens openapi_source_binding.value into kind/secret_id/connection_id/text_value", () => { | |
| 87 | + const db = openDatabase(dbPath); | |
| 88 | + db.exec(PRE_0007_SQL); | |
| 89 | + stampPriorMigrationsApplied(db); | |
| 90 | + | |
| 91 | + // Seed three bindings, one per kind. | |
| 92 | + const insert = db.prepare( | |
| 93 | + "INSERT INTO openapi_source_binding (id, source_id, source_scope_id, target_scope_id, slot, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", | |
| 94 | + ); | |
| 95 | + const now = Date.now(); | |
| 96 | + insert.run( | |
| 97 | + "b1", | |
| 98 | + "src", | |
| 99 | + "default-scope", | |
| 100 | + "default-scope", | |
| 101 | + "header:authorization", | |
| 102 | + JSON.stringify({ kind: "secret", secretId: "tok-secret" }), | |
| 103 | + now, | |
| 104 | + now, | |
| 105 | + ); | |
| 106 | + insert.run( | |
| 107 | + "b2", | |
| 108 | + "src", | |
| 109 | + "default-scope", | |
| 110 | + "default-scope", | |
| 111 | + "oauth2:default:connection", | |
| 112 | + JSON.stringify({ kind: "connection", connectionId: "conn-1" }), | |
| 113 | + now, | |
| 114 | + now, | |
| 115 | + ); | |
| 116 | + insert.run( | |
| 117 | + "b3", | |
| 118 | + "src", | |
| 119 | + "default-scope", | |
| 120 | + "default-scope", | |
| 121 | + "header:x-static", | |
| 122 | + JSON.stringify({ kind: "text", text: "literal" }), | |
| 123 | + now, | |
| 124 | + now, | |
| 125 | + ); | |
| 126 | + | |
| 127 | + // Need the parent openapi_source row so the source_id FK ergonomics | |
| 128 | + // are satisfied for any cascading delete logic, though the binding | |
| 129 | + // table has no DB-level FK, code paths assume the parent exists. | |
| 130 | + db.prepare( | |
| 131 | + "INSERT INTO openapi_source (scope_id, id, name, spec, invocation_config) VALUES (?, ?, ?, ?, ?)", | |
| 132 | + ).run("default-scope", "src", "Source", "{}", "{}"); | |
| 133 | + | |
| 134 | + closeDatabase(db); | |
| 135 | + | |
| 136 | + const drizzleSqlite = openDatabase(dbPath); | |
| 137 | + const drizzleDb = drizzle(drizzleSqlite); | |
| 138 | + migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | |
| 139 | + closeDatabase(drizzleSqlite); | |
| 76 | 140 | |
| 77 | − const after = new Database(dbPath, { readonly: true }); | |
| 78 | − const rows = after | |
| 141 | + const after = openDatabase(dbPath, { readonly: true }); | |
| 142 | + const rows = Schema.decodeUnknownSync(Schema.Array(BindingRow))( | |
| 143 | + after | |
| 79 | 144 | .prepare( |
| 80 | 145 | "SELECT id, kind, secret_id, connection_id, text_value FROM openapi_source_binding ORDER BY id", |
| 81 | 146 | ) |
| 82 | − .all() as ReadonlyArray<{ | |
| 83 | − id: string; | |
| 84 | − kind: string; | |
| 85 | − secret_id: string | null; | |
| 86 | − connection_id: string | null; | |
| 87 | − text_value: string | null; | |
| 88 | − }>; | |
| 89 | − expect(rows).toHaveLength(3); | |
| 90 | − expect(rows[0]).toMatchObject({ | |
| 91 | − id: "b1", | |
| 92 | − kind: "secret", | |
| 93 | − secret_id: "tok-secret", | |
| 94 | − connection_id: null, | |
| 95 | − text_value: null, | |
| 96 | − }); | |
| 97 | − expect(rows[1]).toMatchObject({ | |
| 98 | − id: "b2", | |
| 99 | − kind: "connection", | |
| 100 | − secret_id: null, | |
| 101 | − connection_id: "conn-1", | |
| 102 | − text_value: null, | |
| 103 | − }); | |
| 104 | − expect(rows[2]).toMatchObject({ | |
| 105 | − id: "b3", | |
| 106 | − kind: "text", | |
| 107 | − secret_id: null, | |
| 108 | − connection_id: null, | |
| 109 | − text_value: "literal", | |
| 110 | − }); | |
| 111 | − // value json column dropped. | |
| 112 | − const cols = after | |
| 113 | − .prepare("PRAGMA table_info('openapi_source_binding')") | |
| 114 | − .all() as ReadonlyArray<{ name: string }>; | |
| 115 | − expect(cols.some((c) => c.name === "value")).toBe(false); | |
| 116 | − after.close(); | |
| 117 | − } finally { | |
| 118 | − rmSync(dir, { recursive: true, force: true }); | |
| 119 | − } | |
| 147 | + .all(), | |
| 148 | + ); | |
| 149 | + expect(rows).toHaveLength(3); | |
| 150 | + expect(rows[0]).toMatchObject({ | |
| 151 | + id: "b1", | |
| 152 | + kind: "secret", | |
| 153 | + secret_id: "tok-secret", | |
| 154 | + connection_id: null, | |
| 155 | + text_value: null, | |
| 156 | + }); | |
| 157 | + expect(rows[1]).toMatchObject({ | |
| 158 | + id: "b2", | |
| 159 | + kind: "connection", | |
| 160 | + secret_id: null, | |
| 161 | + connection_id: "conn-1", | |
| 162 | + text_value: null, | |
| 163 | + }); | |
| 164 | + expect(rows[2]).toMatchObject({ | |
| 165 | + id: "b3", | |
| 166 | + kind: "text", | |
| 167 | + secret_id: null, | |
| 168 | + connection_id: null, | |
| 169 | + text_value: "literal", | |
| 170 | + }); | |
| 171 | + // value json column dropped. | |
| 172 | + const cols = Schema.decodeUnknownSync(Schema.Array(TableInfoRow))( | |
| 173 | + after.prepare("PRAGMA table_info('openapi_source_binding')").all(), | |
| 174 | + ); | |
| 175 | + expect(cols.some((c) => c.name === "value")).toBe(false); | |
| 120 | 176 | }); |
| 121 | 177 | |
| 122 | 178 | it("explodes query_params and specFetchCredentials json into child rows", () => { |
| 123 | − const dir = mkdtempSync(join(tmpdir(), "openapi-mig-")); | |
| 124 | − const dbPath = join(dir, "test.sqlite"); | |
| 125 | − try { | |
| 126 | − const db = new Database(dbPath); | |
| 127 | − db.exec(PRE_0007_SQL); | |
| 128 | − stampPriorMigrationsApplied(db); | |
| 129 | − | |
| 130 | − const queryParams = { | |
| 131 | − api_key: { secretId: "qp-secret" }, | |
| 132 | − flag: "true", | |
| 133 | − }; | |
| 134 | − const invocationConfig = { | |
| 135 | − specFetchCredentials: { | |
| 136 | − headers: { | |
| 137 | − Authorization: { secretId: "fetch-tok", prefix: "Bearer " }, | |
| 138 | − }, | |
| 139 | − queryParams: { token: { secretId: "fetch-qp" } }, | |
| 179 | + const db = openDatabase(dbPath); | |
| 180 | + db.exec(PRE_0007_SQL); | |
| 181 | + stampPriorMigrationsApplied(db); | |
| 182 | + | |
| 183 | + const queryParams = { | |
| 184 | + api_key: { secretId: "qp-secret" }, | |
| 185 | + flag: "true", | |
| 186 | + }; | |
| 187 | + const invocationConfig = { | |
| 188 | + specFetchCredentials: { | |
| 189 | + headers: { | |
| 190 | + Authorization: { secretId: "fetch-tok", prefix: "Bearer " }, | |
| 140 | 191 | }, |
| 141 | − }; | |
| 142 | − | |
| 143 | − db.prepare( | |
| 144 | − "INSERT INTO openapi_source (scope_id, id, name, spec, query_params, invocation_config) VALUES (?, ?, ?, ?, ?, ?)", | |
| 145 | − ).run( | |
| 146 | − "default-scope", | |
| 147 | − "src", | |
| 148 | − "Source", | |
| 149 | − "{}", | |
| 150 | − JSON.stringify(queryParams), | |
| 151 | − JSON.stringify(invocationConfig), | |
| 152 | − ); | |
| 192 | + queryParams: { token: { secretId: "fetch-qp" } }, | |
| 193 | + }, | |
| 194 | + }; | |
| 153 | 195 | |
| 154 | − db.close(); | |
| 196 | + db.prepare( | |
| 197 | + "INSERT INTO openapi_source (scope_id, id, name, spec, query_params, invocation_config) VALUES (?, ?, ?, ?, ?, ?)", | |
| 198 | + ).run( | |
| 199 | + "default-scope", | |
| 200 | + "src", | |
| 201 | + "Source", | |
| 202 | + "{}", | |
| 203 | + JSON.stringify(queryParams), | |
| 204 | + JSON.stringify(invocationConfig), | |
| 205 | + ); | |
| 155 | 206 | |
| 156 | − const drizzleDb = drizzle(new Database(dbPath)); | |
| 157 | − migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | |
| 207 | + closeDatabase(db); | |
| 158 | 208 | |
| 159 | − const after = new Database(dbPath, { readonly: true }); | |
| 209 | + const drizzleSqlite = openDatabase(dbPath); | |
| 210 | + const drizzleDb = drizzle(drizzleSqlite); | |
| 211 | + migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | |
| 212 | + closeDatabase(drizzleSqlite); | |
| 160 | 213 | |
| 161 | − const qpRows = after | |
| 214 | + const after = openDatabase(dbPath, { readonly: true }); | |
| 215 | + | |
| 216 | + const qpRows = Schema.decodeUnknownSync(Schema.Array(QueryParamRow))( | |
| 217 | + after | |
| 162 | 218 | .prepare( |
| 163 | 219 | "SELECT name, kind, text_value, secret_id FROM openapi_source_query_param WHERE source_id = ? ORDER BY name", |
| 164 | 220 | ) |
| 165 | − .all("src") as ReadonlyArray<{ | |
| 166 | − name: string; | |
| 167 | − kind: string; | |
| 168 | − text_value: string | null; | |
| 169 | − secret_id: string | null; | |
| 170 | − }>; | |
| 171 | − expect(qpRows).toHaveLength(2); | |
| 172 | − const byName = new Map(qpRows.map((r) => [r.name, r])); | |
| 173 | − expect(byName.get("api_key")).toMatchObject({ | |
| 174 | − kind: "secret", | |
| 175 | − secret_id: "qp-secret", | |
| 176 | − }); | |
| 177 | − expect(byName.get("flag")).toMatchObject({ | |
| 178 | − kind: "text", | |
| 179 | − text_value: "true", | |
| 180 | − }); | |
| 181 | − | |
| 182 | − const fetchHeaders = after | |
| 221 | + .all("src"), | |
| 222 | + ); | |
| 223 | + expect(qpRows).toHaveLength(2); | |
| 224 | + const byName = new Map(qpRows.map((r) => [r.name, r])); | |
| 225 | + expect(byName.get("api_key")).toMatchObject({ | |
| 226 | + kind: "secret", | |
| 227 | + secret_id: "qp-secret", | |
| 228 | + }); | |
| 229 | + expect(byName.get("flag")).toMatchObject({ | |
| 230 | + kind: "text", | |
| 231 | + text_value: "true", | |
| 232 | + }); | |
| 233 | + | |
| 234 | + const fetchHeaders = Schema.decodeUnknownSync(Schema.Array(FetchHeaderRow))( | |
| 235 | + after | |
| 183 | 236 | .prepare( |
| 184 | 237 | "SELECT name, kind, secret_id, secret_prefix FROM openapi_source_spec_fetch_header WHERE source_id = ?", |
| 185 | 238 | ) |
| 186 | − .all("src") as ReadonlyArray<{ | |
| 187 | − name: string; | |
| 188 | − kind: string; | |
| 189 | − secret_id: string | null; | |
| 190 | − secret_prefix: string | null; | |
| 191 | − }>; | |
| 192 | − expect(fetchHeaders).toHaveLength(1); | |
| 193 | − expect(fetchHeaders[0]).toMatchObject({ | |
| 194 | − name: "Authorization", | |
| 195 | − kind: "secret", | |
| 196 | − secret_id: "fetch-tok", | |
| 197 | − secret_prefix: "Bearer ", | |
| 198 | − }); | |
| 199 | − | |
| 200 | − const fetchQp = after | |
| 239 | + .all("src"), | |
| 240 | + ); | |
| 241 | + expect(fetchHeaders).toHaveLength(1); | |
| 242 | + expect(fetchHeaders[0]).toMatchObject({ | |
| 243 | + name: "Authorization", | |
| 244 | + kind: "secret", | |
| 245 | + secret_id: "fetch-tok", | |
| 246 | + secret_prefix: "Bearer ", | |
| 247 | + }); | |
| 248 | + | |
| 249 | + const fetchQp = Schema.decodeUnknownSync(Schema.Array(FetchQueryParamRow))( | |
| 250 | + after | |
| 201 | 251 | .prepare( |
| 202 | 252 | "SELECT name, secret_id FROM openapi_source_spec_fetch_query_param WHERE source_id = ?", |
| 203 | 253 | ) |
| 204 | − .all("src") as ReadonlyArray<{ name: string; secret_id: string }>; | |
| 205 | − expect(fetchQp).toHaveLength(1); | |
| 206 | − expect(fetchQp[0]).toMatchObject({ name: "token", secret_id: "fetch-qp" }); | |
| 207 | − | |
| 208 | − // Old json columns dropped. | |
| 209 | − const cols = after | |
| 210 | − .prepare("PRAGMA table_info('openapi_source')") | |
| 211 | − .all() as ReadonlyArray<{ name: string }>; | |
| 212 | − expect(cols.some((c) => c.name === "query_params")).toBe(false); | |
| 213 | − expect(cols.some((c) => c.name === "invocation_config")).toBe(false); | |
| 214 | − after.close(); | |
| 215 | − } finally { | |
| 216 | − rmSync(dir, { recursive: true, force: true }); | |
| 217 | − } | |
| 254 | + .all("src"), | |
| 255 | + ); | |
| 256 | + expect(fetchQp).toHaveLength(1); | |
| 257 | + expect(fetchQp[0]).toMatchObject({ name: "token", secret_id: "fetch-qp" }); | |
| 258 | + | |
| 259 | + // Old json columns dropped. | |
| 260 | + const cols = Schema.decodeUnknownSync(Schema.Array(TableInfoRow))( | |
| 261 | + after.prepare("PRAGMA table_info('openapi_source')").all(), | |
| 262 | + ); | |
| 263 | + expect(cols.some((c) => c.name === "query_params")).toBe(false); | |
| 264 | + expect(cols.some((c) => c.name === "invocation_config")).toBe(false); | |
| 218 | 265 | }); |
| 219 | 266 | |
| 220 | 267 | it("survives empty / missing json on bindings and sources", () => { |
| 221 | − const dir = mkdtempSync(join(tmpdir(), "openapi-mig-")); | |
| 222 | − const dbPath = join(dir, "test.sqlite"); | |
| 223 | − try { | |
| 224 | − const db = new Database(dbPath); | |
| 225 | − db.exec(PRE_0007_SQL); | |
| 226 | − stampPriorMigrationsApplied(db); | |
| 227 | − | |
| 228 | − // Source with empty invocation_config and no query_params. | |
| 229 | − db.prepare( | |
| 230 | − "INSERT INTO openapi_source (scope_id, id, name, spec, invocation_config) VALUES (?, ?, ?, ?, ?)", | |
| 231 | − ).run("default-scope", "bare", "Bare", "{}", JSON.stringify({})); | |
| 268 | + const db = openDatabase(dbPath); | |
| 269 | + db.exec(PRE_0007_SQL); | |
| 270 | + stampPriorMigrationsApplied(db); | |
| 232 | 271 | |
| 233 | − db.close(); | |
| 234 | − const drizzleDb = drizzle(new Database(dbPath)); | |
| 235 | − migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | |
| 236 | − | |
| 237 | − const after = new Database(dbPath, { readonly: true }); | |
| 238 | − const qpCount = ( | |
| 239 | − after | |
| 240 | − .prepare( | |
| 241 | − "SELECT count(*) as n FROM openapi_source_query_param WHERE source_id = ?", | |
| 242 | − ) | |
| 243 | − .get("bare") as { n: number } | |
| 244 | − ).n; | |
| 245 | − expect(qpCount).toBe(0); | |
| 246 | − after.close(); | |
| 247 | − } finally { | |
| 248 | − rmSync(dir, { recursive: true, force: true }); | |
| 249 | − } | |
| 272 | + // Source with empty invocation_config and no query_params. | |
| 273 | + db.prepare( | |
| 274 | + "INSERT INTO openapi_source (scope_id, id, name, spec, invocation_config) VALUES (?, ?, ?, ?, ?)", | |
| 275 | + ).run("default-scope", "bare", "Bare", "{}", JSON.stringify({})); | |
| 276 | + | |
| 277 | + closeDatabase(db); | |
| 278 | + const drizzleSqlite = openDatabase(dbPath); | |
| 279 | + const drizzleDb = drizzle(drizzleSqlite); | |
| 280 | + migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER }); | |
| 281 | + closeDatabase(drizzleSqlite); | |
| 282 | + | |
| 283 | + const after = openDatabase(dbPath, { readonly: true }); | |
| 284 | + const qpCount = Schema.decodeUnknownSync(CountRow)( | |
| 285 | + after | |
| 286 | + .prepare( | |
| 287 | + "SELECT count(*) as n FROM openapi_source_query_param WHERE source_id = ?", | |
| 288 | + ) | |
| 289 | + .get("bare"), | |
| 290 | + ).n; | |
| 291 | + expect(qpCount).toBe(0); | |
| 250 | 292 | }); |
| 251 | 293 | }); |