History

Parse local migration test rows with Schema

Rhys Sullivan committed 6 months ago

Commit daa17631 parent b039b6e2 files · +407 −364Browse files

Unified · Split

2 files
  1. apps/local/src/server/migrate-google-discovery-bindings.test.ts +179 −172
  2. apps/local/src/server/migrate-graphql-bindings.test.ts +228 −192
apps/local/src/server/migrate-google-discovery-bindings.test.ts+179−172
@@ -4,8 +4,9 @@ · 3 unmodified lines
4// containing auth/credentials), runs the migration, asserts the new4// containing auth/credentials), runs the migration, asserts the new
5// columns and child tables are populated.5// columns and child tables are populated.
6 6
7import { describe, expect, it } from "@effect/vitest";7import { afterEach, describe, expect, it } from "@effect/vitest";
8import { Database } from "bun:sqlite";8import { Database } from "bun:sqlite";
9import { Schema } from "effect";
9import { mkdtempSync, rmSync } from "node:fs";10import { mkdtempSync, rmSync } from "node:fs";
10import { join } from "node:path";11import { join } from "node:path";
11import { tmpdir } from "node:os";12import { tmpdir } from "node:os";
@@ -16,189 +17,195 @@ import { PRE_0007_SQL, stampPriorMigrationsApplied } from "./__test-helpers__/pr · 4 unmodified lines
16 17
17const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle");18const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle");
18 19
19describe("0007_normalize_plugin_secret_refs (google-discovery)", () => {20const migratedConfig = Schema.Struct({
20 it("flattens oauth2 auth into columns", () => {21 auth: Schema.optional(Schema.Unknown),
21 const dir = mkdtempSync(join(tmpdir(), "gd-mig-"));22 service: Schema.String,
22 const dbPath = join(dir, "test.sqlite");23});
23 try {24const decodeMigratedConfig = Schema.decodeUnknownSync(Schema.fromJsonString(migratedConfig));
24 const db = new Database(dbPath);
25 db.exec(PRE_0007_SQL);
26 stampPriorMigrationsApplied(db);
27
28 db.prepare(
29 "INSERT INTO google_discovery_source (scope_id, id, name, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
30 ).run(
31 "default-scope",
32 "drive",
33 "Drive",
34 JSON.stringify({
35 name: "Drive",
36 discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest",
37 service: "drive",
38 version: "v3",
39 rootUrl: "https://www.googleapis.com/",
40 servicePath: "drive/v3/",
41 auth: {
42 kind: "oauth2",
43 connectionId: "conn-1",
44 clientIdSecretId: "client-id",
45 clientSecretSecretId: "client-secret",
46 scopes: ["https://www.googleapis.com/auth/drive"],
47 },
48 }),
49 Date.now(),
50 Date.now(),
51 );
52 25
53 db.close();26const tempDirs = new Set<string>();
54 const drizzleDb = drizzle(new Database(dbPath));
55 migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER });
56 27
57 const after = new Database(dbPath, { readonly: true });28const createTempDbPath = () => {
58 const row = after29 const dir = mkdtempSync(join(tmpdir(), "gd-mig-"));
59 .prepare(30 tempDirs.add(dir);
60 "SELECT auth_kind, auth_connection_id, auth_client_id_secret_id, auth_client_secret_secret_id, auth_scopes, config FROM google_discovery_source WHERE id = ?",31 return join(dir, "test.sqlite");
61 )32};
62 .get("drive") as Record<string, string | null>;33
63 expect(row.auth_kind).toBe("oauth2");34describe("0007_normalize_plugin_secret_refs (google-discovery)", () => {
64 expect(row.auth_connection_id).toBe("conn-1");35 afterEach(() => {
65 expect(row.auth_client_id_secret_id).toBe("client-id");36 for (const dir of tempDirs) {
66 expect(row.auth_client_secret_secret_id).toBe("client-secret");
67 // auth_scopes column is text-typed (string[] gets stored as JSON in sqlite).
68 expect(row.auth_scopes).toContain("drive");
69 // The auth key should be stripped from config json.
70 const config = JSON.parse(row.config!);
71 expect(config.auth).toBeUndefined();
72 expect(config.service).toBe("drive");
73 after.close();
74 } finally {
75 rmSync(dir, { recursive: true, force: true });37 rmSync(dir, { recursive: true, force: true });
76 }38 }
39 tempDirs.clear();
40 });
41
42 it("flattens oauth2 auth into columns", () => {
43 const dbPath = createTempDbPath();
44 const db = new Database(dbPath);
45 db.exec(PRE_0007_SQL);
46 stampPriorMigrationsApplied(db);
47
48 db.prepare(
49 "INSERT INTO google_discovery_source (scope_id, id, name, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
50 ).run(
51 "default-scope",
52 "drive",
53 "Drive",
54 JSON.stringify({
55 name: "Drive",
56 discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest",
57 service: "drive",
58 version: "v3",
59 rootUrl: "https://www.googleapis.com/",
60 servicePath: "drive/v3/",
61 auth: {
62 kind: "oauth2",
63 connectionId: "conn-1",
64 clientIdSecretId: "client-id",
65 clientSecretSecretId: "client-secret",
66 scopes: ["https://www.googleapis.com/auth/drive"],
67 },
68 }),
69 Date.now(),
70 Date.now(),
71 );
72
73 db.close();
74 const drizzleDb = drizzle(new Database(dbPath));
75 migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER });
76
77 const after = new Database(dbPath, { readonly: true });
78 const row = after
79 .prepare(
80 "SELECT auth_kind, auth_connection_id, auth_client_id_secret_id, auth_client_secret_secret_id, auth_scopes, config FROM google_discovery_source WHERE id = ?",
81 )
82 .get("drive") as Record<string, string | null>;
83 expect(row.auth_kind).toBe("oauth2");
84 expect(row.auth_connection_id).toBe("conn-1");
85 expect(row.auth_client_id_secret_id).toBe("client-id");
86 expect(row.auth_client_secret_secret_id).toBe("client-secret");
87 // auth_scopes column is text-typed (string[] gets stored as JSON in sqlite).
88 expect(row.auth_scopes).toContain("drive");
89 // The auth key should be stripped from config json.
90 const config = decodeMigratedConfig(row.config);
91 expect(config.auth).toBeUndefined();
92 expect(config.service).toBe("drive");
93 after.close();
77 });94 });
78 95
79 it("explodes credentials.headers and queryParams into child rows", () => {96 it("explodes credentials.headers and queryParams into child rows", () => {
80 const dir = mkdtempSync(join(tmpdir(), "gd-mig-"));97 const dbPath = createTempDbPath();
81 const dbPath = join(dir, "test.sqlite");98 const db = new Database(dbPath);
82 try {99 db.exec(PRE_0007_SQL);
83 const db = new Database(dbPath);100 stampPriorMigrationsApplied(db);
84 db.exec(PRE_0007_SQL);101
85 stampPriorMigrationsApplied(db);102 db.prepare(
86 103 "INSERT INTO google_discovery_source (scope_id, id, name, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
87 db.prepare(104 ).run(
88 "INSERT INTO google_discovery_source (scope_id, id, name, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",105 "default-scope",
89 ).run(106 "with-creds",
90 "default-scope",107 "With Creds",
91 "with-creds",108 JSON.stringify({
92 "With Creds",109 name: "With Creds",
93 JSON.stringify({110 discoveryUrl: "https://example.com/discovery",
94 name: "With Creds",111 service: "svc",
95 discoveryUrl: "https://example.com/discovery",112 version: "v1",
96 service: "svc",113 rootUrl: "https://example.com/",
97 version: "v1",114 servicePath: "svc/v1/",
98 rootUrl: "https://example.com/",115 auth: { kind: "none" },
99 servicePath: "svc/v1/",116 credentials: {
100 auth: { kind: "none" },117 headers: {
101 credentials: {118 "X-Static": "literal",
102 headers: {119 Authorization: { secretId: "tok-secret", prefix: "Bearer " },
103 "X-Static": "literal",
104 Authorization: { secretId: "tok-secret", prefix: "Bearer " },
105 },
106 queryParams: {
107 api_key: { secretId: "key-secret" },
108 },
109 },120 },
110 }),121 queryParams: {
111 Date.now(),122 api_key: { secretId: "key-secret" },
112 Date.now(),123 },
113 );124 },
114 125 }),
115 db.close();126 Date.now(),
116 const drizzleDb = drizzle(new Database(dbPath));127 Date.now(),
117 migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER });128 );
118 129
119 const after = new Database(dbPath, { readonly: true });130 db.close();
120 const headers = after131 const drizzleDb = drizzle(new Database(dbPath));
121 .prepare(132 migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER });
122 "SELECT name, kind, text_value, secret_id, secret_prefix FROM google_discovery_source_credential_header WHERE source_id = ? ORDER BY name",133
123 )134 const after = new Database(dbPath, { readonly: true });
124 .all("with-creds") as ReadonlyArray<Record<string, string | null>>;135 const headers = after
125 expect(headers).toHaveLength(2);136 .prepare(
126 const byName = new Map(headers.map((h) => [h.name!, h]));137 "SELECT name, kind, text_value, secret_id, secret_prefix FROM google_discovery_source_credential_header WHERE source_id = ? ORDER BY name",
127 expect(byName.get("X-Static")).toMatchObject({138 )
128 kind: "text",139 .all("with-creds") as ReadonlyArray<Record<string, string | null>>;
129 text_value: "literal",140 expect(headers).toHaveLength(2);
130 });141 const byName = new Map(headers.map((h) => [h.name!, h]));
131 expect(byName.get("Authorization")).toMatchObject({142 expect(byName.get("X-Static")).toMatchObject({
132 kind: "secret",143 kind: "text",
133 secret_id: "tok-secret",144 text_value: "literal",
134 secret_prefix: "Bearer ",145 });
135 });146 expect(byName.get("Authorization")).toMatchObject({
136 147 kind: "secret",
137 const params = after148 secret_id: "tok-secret",
138 .prepare(149 secret_prefix: "Bearer ",
139 "SELECT name, secret_id FROM google_discovery_source_credential_query_param WHERE source_id = ?",150 });
140 )151
141 .all("with-creds") as ReadonlyArray<Record<string, string>>;152 const params = after
142 expect(params).toHaveLength(1);153 .prepare(
143 expect(params[0]).toMatchObject({ name: "api_key", secret_id: "key-secret" });154 "SELECT name, secret_id FROM google_discovery_source_credential_query_param WHERE source_id = ?",
144 155 )
145 after.close();156 .all("with-creds") as ReadonlyArray<Record<string, string>>;
146 } finally {157 expect(params).toHaveLength(1);
147 rmSync(dir, { recursive: true, force: true });158 expect(params[0]).toMatchObject({ name: "api_key", secret_id: "key-secret" });
148 }159
160 after.close();
149 });161 });
150 162
151 it("survives auth.kind=none with no credentials", () => {163 it("survives auth.kind=none with no credentials", () => {
152 const dir = mkdtempSync(join(tmpdir(), "gd-mig-"));164 const dbPath = createTempDbPath();
153 const dbPath = join(dir, "test.sqlite");165 const db = new Database(dbPath);
154 try {166 db.exec(PRE_0007_SQL);
155 const db = new Database(dbPath);167 stampPriorMigrationsApplied(db);
156 db.exec(PRE_0007_SQL);168
157 stampPriorMigrationsApplied(db);169 db.prepare(
158 170 "INSERT INTO google_discovery_source (scope_id, id, name, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
159 db.prepare(171 ).run(
160 "INSERT INTO google_discovery_source (scope_id, id, name, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",172 "default-scope",
161 ).run(173 "bare",
162 "default-scope",174 "Bare",
163 "bare",175 JSON.stringify({
164 "Bare",176 name: "Bare",
165 JSON.stringify({177 discoveryUrl: "https://example.com/discovery",
166 name: "Bare",178 service: "svc",
167 discoveryUrl: "https://example.com/discovery",179 version: "v1",
168 service: "svc",180 rootUrl: "https://example.com/",
169 version: "v1",181 servicePath: "svc/v1/",
170 rootUrl: "https://example.com/",182 auth: { kind: "none" },
171 servicePath: "svc/v1/",183 }),
172 auth: { kind: "none" },184 Date.now(),
173 }),185 Date.now(),
174 Date.now(),186 );
175 Date.now(),187
176 );188 db.close();
177 189 const drizzleDb = drizzle(new Database(dbPath));
178 db.close();190 migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER });
179 const drizzleDb = drizzle(new Database(dbPath));191
180 migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER });192 const after = new Database(dbPath, { readonly: true });
181 193 const row = after
182 const after = new Database(dbPath, { readonly: true });194 .prepare(
183 const row = after195 "SELECT auth_kind, auth_connection_id, auth_scopes FROM google_discovery_source WHERE id = ?",
196 )
197 .get("bare") as Record<string, string | null>;
198 expect(row.auth_kind).toBe("none");
199 expect(row.auth_connection_id).toBeNull();
200
201 const headerCount = (
202 after
184 .prepare(203 .prepare(
185 "SELECT auth_kind, auth_connection_id, auth_scopes FROM google_discovery_source WHERE id = ?",204 "SELECT count(*) as n FROM google_discovery_source_credential_header WHERE source_id = ?",
186 )205 )
187 .get("bare") as Record<string, string | null>;206 .get("bare") as { n: number }
188 expect(row.auth_kind).toBe("none");207 ).n;
189 expect(row.auth_connection_id).toBeNull();208 expect(headerCount).toBe(0);
190 209 after.close();
191 const headerCount = (
192 after
193 .prepare(
194 "SELECT count(*) as n FROM google_discovery_source_credential_header WHERE source_id = ?",
195 )
196 .get("bare") as { n: number }
197 ).n;
198 expect(headerCount).toBe(0);
199 after.close();
200 } finally {
201 rmSync(dir, { recursive: true, force: true });
202 }
203 });210 });
204});211});
apps/local/src/server/migrate-graphql-bindings.test.ts+228−192
@@ -4,8 +4,9 @@ · 3 unmodified lines
4// run the migration, assert that the JSON unpacks into the new4// run the migration, assert that the JSON unpacks into the new
5// normalized columns / child tables and that the JSON columns are gone.5// normalized columns / child tables and that the JSON columns are gone.
6 6
7import { describe, expect, it } from "@effect/vitest";7import { afterEach, beforeEach, describe, expect, it } from "@effect/vitest";
8import { Database } from "bun:sqlite";8import { Database } from "bun:sqlite";
9import { Schema } from "effect";
9import { mkdtempSync, rmSync } from "node:fs";10import { mkdtempSync, rmSync } from "node:fs";
10import { join } from "node:path";11import { join } from "node:path";
11import { tmpdir } from "node:os";12import { tmpdir } from "node:os";
@@ -16,231 +17,266 @@ import { PRE_0007_SQL, stampPriorMigrationsApplied } from "./__test-helpers__/pr · 4 unmodified lines
16 17
17const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle");18const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle");
18 19
20const NullableString = Schema.NullOr(Schema.String);
21
22const GraphqlAuthRow = Schema.Struct({
23 auth_kind: Schema.String,
24 auth_connection_id: NullableString,
25});
26
27const TableInfoRow = Schema.Struct({
28 name: Schema.String,
29});
30
31const GraphqlHeaderRow = Schema.Struct({
32 name: Schema.String,
33 kind: Schema.String,
34 text_value: NullableString,
35 secret_id: NullableString,
36 secret_prefix: NullableString,
37});
38
39const GraphqlQueryParamRow = Schema.Struct({
40 kind: Schema.String,
41 secret_id: Schema.String,
42});
43
44const CountRow = Schema.Struct({
45 n: Schema.Number,
46});
47
48const GraphqlHeaderIdRow = Schema.Struct({
49 id: Schema.String,
50 source_id: Schema.String,
51 name: Schema.String,
52 text_value: Schema.String,
53});
54
55const decodeAuthRow = Schema.decodeUnknownSync(GraphqlAuthRow);
56const decodeTableInfoRows = Schema.decodeUnknownSync(Schema.Array(TableInfoRow));
57const decodeHeaderRows = Schema.decodeUnknownSync(Schema.Array(GraphqlHeaderRow));
58const decodeQueryParamRow = Schema.decodeUnknownSync(GraphqlQueryParamRow);
59const decodeCountRow = Schema.decodeUnknownSync(CountRow);
60const decodeHeaderIdRows = Schema.decodeUnknownSync(
61 Schema.Array(GraphqlHeaderIdRow),
62);
63
64let dir: string;
65
66beforeEach(() => {
67 dir = mkdtempSync(join(tmpdir(), "graphql-mig-"));
68});
69
70afterEach(() => {
71 rmSync(dir, { recursive: true, force: true });
72});
73
19describe("0007_normalize_plugin_secret_refs (graphql)", () => {74describe("0007_normalize_plugin_secret_refs (graphql)", () => {
20 it("flattens auth json into auth_kind/auth_connection_id columns", () => {75 it("flattens auth json into auth_kind/auth_connection_id columns", () => {
21 const dir = mkdtempSync(join(tmpdir(), "graphql-mig-"));
22 const dbPath = join(dir, "test.sqlite");76 const dbPath = join(dir, "test.sqlite");
23 try {77 const db = new Database(dbPath);
24 const db = new Database(dbPath);78 db.exec(PRE_0007_SQL);
25 db.exec(PRE_0007_SQL);79 stampPriorMigrationsApplied(db);
26 stampPriorMigrationsApplied(db);80
27 81 db.prepare(
28 db.prepare(82 "INSERT INTO graphql_source (scope_id, id, name, endpoint, auth) VALUES (?, ?, ?, ?, ?)",
29 "INSERT INTO graphql_source (scope_id, id, name, endpoint, auth) VALUES (?, ?, ?, ?, ?)",83 ).run(
30 ).run(84 "default-scope",
31 "default-scope",85 "github",
32 "github",86 "GitHub",
33 "GitHub",87 "https://api.github.com/graphql",
34 "https://api.github.com/graphql",88 JSON.stringify({ kind: "oauth2", connectionId: "conn-1" }),
35 JSON.stringify({ kind: "oauth2", connectionId: "conn-1" }),89 );
36 );90
37 91 db.close();
38 db.close();92
39 93 const drizzleDb = drizzle(new Database(dbPath));
40 const drizzleDb = drizzle(new Database(dbPath));94 migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER });
41 migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER });95
42 96 const after = new Database(dbPath, { readonly: true });
43 const after = new Database(dbPath, { readonly: true });97 const row = decodeAuthRow(
44 const row = after98 after
45 .prepare(99 .prepare(
46 "SELECT auth_kind, auth_connection_id FROM graphql_source WHERE id = ?",100 "SELECT auth_kind, auth_connection_id FROM graphql_source WHERE id = ?",
47 )101 )
48 .get("github") as { auth_kind: string; auth_connection_id: string };102 .get("github"),
49 expect(row.auth_kind).toBe("oauth2");103 );
50 expect(row.auth_connection_id).toBe("conn-1");104 expect(row.auth_kind).toBe("oauth2");
51 // Old json column is gone.105 expect(row.auth_connection_id).toBe("conn-1");
52 const cols = after106 // Old json column is gone.
107 const cols = decodeTableInfoRows(
108 after
53 .prepare("PRAGMA table_info('graphql_source')")109 .prepare("PRAGMA table_info('graphql_source')")
54 .all() as ReadonlyArray<{ name: string }>;110 .all(),
55 expect(cols.some((c) => c.name === "auth")).toBe(false);111 );
56 expect(cols.some((c) => c.name === "headers")).toBe(false);112 expect(cols.some((c) => c.name === "auth")).toBe(false);
57 expect(cols.some((c) => c.name === "query_params")).toBe(false);113 expect(cols.some((c) => c.name === "headers")).toBe(false);
58 after.close();114 expect(cols.some((c) => c.name === "query_params")).toBe(false);
59 } finally {115 after.close();
60 rmSync(dir, { recursive: true, force: true });
61 }
62 });116 });
63 117
64 it("explodes header/query_param json into child rows", () => {118 it("explodes header/query_param json into child rows", () => {
65 const dir = mkdtempSync(join(tmpdir(), "graphql-mig-"));
66 const dbPath = join(dir, "test.sqlite");119 const dbPath = join(dir, "test.sqlite");
67 try {120 const db = new Database(dbPath);
68 const db = new Database(dbPath);121 db.exec(PRE_0007_SQL);
69 db.exec(PRE_0007_SQL);122 stampPriorMigrationsApplied(db);
70 stampPriorMigrationsApplied(db);123
71 124 const headers = {
72 const headers = {125 // Literal text header.
73 // Literal text header.126 "X-Static": "literal-value",
74 "X-Static": "literal-value",127 // Secret-backed header without prefix.
75 // Secret-backed header without prefix.128 Authorization: { secretId: "sec-token" },
76 Authorization: { secretId: "sec-token" },129 // Secret-backed with prefix.
77 // Secret-backed with prefix.130 "X-Bearer": { secretId: "sec-bearer", prefix: "Bearer " },
78 "X-Bearer": { secretId: "sec-bearer", prefix: "Bearer " },131 };
79 };132 const queryParams = {
80 const queryParams = {133 api_key: { secretId: "sec-key" },
81 api_key: { secretId: "sec-key" },134 };
82 };135
83 136 db.prepare(
84 db.prepare(137 "INSERT INTO graphql_source (scope_id, id, name, endpoint, headers, query_params, auth) VALUES (?, ?, ?, ?, ?, ?, ?)",
85 "INSERT INTO graphql_source (scope_id, id, name, endpoint, headers, query_params, auth) VALUES (?, ?, ?, ?, ?, ?, ?)",138 ).run(
86 ).run(139 "default-scope",
87 "default-scope",140 "example",
88 "example",141 "Example",
89 "Example",142 "https://example.com/graphql",
90 "https://example.com/graphql",143 JSON.stringify(headers),
91 JSON.stringify(headers),144 JSON.stringify(queryParams),
92 JSON.stringify(queryParams),145 JSON.stringify({ kind: "none" }),
93 JSON.stringify({ kind: "none" }),146 );
94 );147
95 148 db.close();
96 db.close();149
97 150 const drizzleDb = drizzle(new Database(dbPath));
98 const drizzleDb = drizzle(new Database(dbPath));151 migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER });
99 migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER });152
100 153 const after = new Database(dbPath, { readonly: true });
101 const after = new Database(dbPath, { readonly: true });154 const headerRows = decodeHeaderRows(
102 const headerRows = after155 after
103 .prepare(156 .prepare(
104 "SELECT name, kind, text_value, secret_id, secret_prefix FROM graphql_source_header WHERE source_id = ? ORDER BY name",157 "SELECT name, kind, text_value, secret_id, secret_prefix FROM graphql_source_header WHERE source_id = ? ORDER BY name",
105 )158 )
106 .all("example") as ReadonlyArray<{159 .all("example"),
107 name: string;160 );
108 kind: string;161 expect(headerRows).toHaveLength(3);
109 text_value: string | null;162
110 secret_id: string | null;163 const byName = new Map(headerRows.map((r) => [r.name, r]));
111 secret_prefix: string | null;164 expect(byName.get("X-Static")).toMatchObject({
112 }>;165 kind: "text",
113 expect(headerRows).toHaveLength(3);166 text_value: "literal-value",
114 167 secret_id: null,
115 const byName = new Map(headerRows.map((r) => [r.name, r]));168 });
116 expect(byName.get("X-Static")).toMatchObject({169 expect(byName.get("Authorization")).toMatchObject({
117 kind: "text",170 kind: "secret",
118 text_value: "literal-value",171 text_value: null,
119 secret_id: null,172 secret_id: "sec-token",
120 });173 secret_prefix: null,
121 expect(byName.get("Authorization")).toMatchObject({174 });
122 kind: "secret",175 expect(byName.get("X-Bearer")).toMatchObject({
123 text_value: null,176 kind: "secret",
124 secret_id: "sec-token",177 secret_id: "sec-bearer",
125 secret_prefix: null,178 secret_prefix: "Bearer ",
126 });179 });
127 expect(byName.get("X-Bearer")).toMatchObject({180
128 kind: "secret",181 const paramRow = decodeQueryParamRow(
129 secret_id: "sec-bearer",182 after
130 secret_prefix: "Bearer ",
131 });
132
133 const paramRow = after
134 .prepare(183 .prepare(
135 "SELECT kind, secret_id FROM graphql_source_query_param WHERE source_id = ?",184 "SELECT kind, secret_id FROM graphql_source_query_param WHERE source_id = ?",
136 )185 )
137 .get("example") as { kind: string; secret_id: string };186 .get("example"),
138 expect(paramRow).toMatchObject({ kind: "secret", secret_id: "sec-key" });187 );
188 expect(paramRow).toMatchObject({ kind: "secret", secret_id: "sec-key" });
139 189
140 after.close();190 after.close();
141 } finally {
142 rmSync(dir, { recursive: true, force: true });
143 }
144 });191 });
145 192
146 it("handles graphql_source rows with null json (empty config)", () => {193 it("handles graphql_source rows with null json (empty config)", () => {
147 const dir = mkdtempSync(join(tmpdir(), "graphql-mig-"));
148 const dbPath = join(dir, "test.sqlite");194 const dbPath = join(dir, "test.sqlite");
149 try {195 const db = new Database(dbPath);
150 const db = new Database(dbPath);196 db.exec(PRE_0007_SQL);
151 db.exec(PRE_0007_SQL);197 stampPriorMigrationsApplied(db);
152 stampPriorMigrationsApplied(db);
153 198
154 db.prepare(199 db.prepare(
155 "INSERT INTO graphql_source (scope_id, id, name, endpoint) VALUES (?, ?, ?, ?)",200 "INSERT INTO graphql_source (scope_id, id, name, endpoint) VALUES (?, ?, ?, ?)",
156 ).run("default-scope", "bare", "Bare", "https://bare.example/graphql");201 ).run("default-scope", "bare", "Bare", "https://bare.example/graphql");
157 db.close();202 db.close();
158 203
159 const drizzleDb = drizzle(new Database(dbPath));204 const drizzleDb = drizzle(new Database(dbPath));
160 migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER });205 migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER });
161 206
162 const after = new Database(dbPath, { readonly: true });207 const after = new Database(dbPath, { readonly: true });
163 const row = after208 const row = decodeAuthRow(
209 after
164 .prepare(210 .prepare(
165 "SELECT auth_kind, auth_connection_id FROM graphql_source WHERE id = ?",211 "SELECT auth_kind, auth_connection_id FROM graphql_source WHERE id = ?",
166 )212 )
167 .get("bare") as { auth_kind: string; auth_connection_id: string | null };213 .get("bare"),
168 expect(row.auth_kind).toBe("none");214 );
169 expect(row.auth_connection_id).toBeNull();215 expect(row.auth_kind).toBe("none");
170 216 expect(row.auth_connection_id).toBeNull();
171 const headerCount = (217
172 after218 const headerCount = decodeCountRow(
173 .prepare(219 after
174 "SELECT count(*) as n FROM graphql_source_header WHERE source_id = ?",220 .prepare(
175 )221 "SELECT count(*) as n FROM graphql_source_header WHERE source_id = ?",
176 .get("bare") as { n: number }222 )
177 ).n;223 .get("bare"),
178 expect(headerCount).toBe(0);224 ).n;
179 after.close();225 expect(headerCount).toBe(0);
180 } finally {226 after.close();
181 rmSync(dir, { recursive: true, force: true });
182 }
183 });227 });
184 228
185 it("does not collapse child rows whose source/name pairs share colon-concatenated ids", () => {229 it("does not collapse child rows whose source/name pairs share colon-concatenated ids", () => {
186 const dir = mkdtempSync(join(tmpdir(), "graphql-mig-"));
187 const dbPath = join(dir, "test.sqlite");230 const dbPath = join(dir, "test.sqlite");
188 try {231 const db = new Database(dbPath);
189 const db = new Database(dbPath);232 db.exec(PRE_0007_SQL);
190 db.exec(PRE_0007_SQL);233 stampPriorMigrationsApplied(db);
191 stampPriorMigrationsApplied(db);234
192 235 const insert = db.prepare(
193 const insert = db.prepare(236 "INSERT INTO graphql_source (scope_id, id, name, endpoint, headers) VALUES (?, ?, ?, ?, ?)",
194 "INSERT INTO graphql_source (scope_id, id, name, endpoint, headers) VALUES (?, ?, ?, ?, ?)",237 );
195 );238 insert.run(
196 insert.run(239 "default-scope",
197 "default-scope",240 "a:b",
198 "a:b",241 "First",
199 "First",242 "https://first.example/graphql",
200 "https://first.example/graphql",243 JSON.stringify({ c: "first" }),
201 JSON.stringify({ c: "first" }),244 );
202 );245 insert.run(
203 insert.run(246 "default-scope",
204 "default-scope",247 "a",
205 "a",248 "Second",
206 "Second",249 "https://second.example/graphql",
207 "https://second.example/graphql",250 JSON.stringify({ "b:c": "second" }),
208 JSON.stringify({ "b:c": "second" }),251 );
209 );252 db.close();
210 db.close();253
211 254 const drizzleDb = drizzle(new Database(dbPath));
212 const drizzleDb = drizzle(new Database(dbPath));255 migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER });
213 migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER });256
214 257 const after = new Database(dbPath, { readonly: true });
215 const after = new Database(dbPath, { readonly: true });258 const rows = decodeHeaderIdRows(
216 const rows = after259 after
217 .prepare(260 .prepare(
218 "SELECT id, source_id, name, text_value FROM graphql_source_header ORDER BY source_id, name",261 "SELECT id, source_id, name, text_value FROM graphql_source_header ORDER BY source_id, name",
219 )262 )
220 .all() as ReadonlyArray<{263 .all(),
221 id: string;264 );
222 source_id: string;265 expect(rows).toHaveLength(2);
223 name: string;266 expect(rows).toEqual([
224 text_value: string;267 {
225 }>;268 id: '["a","b:c"]',
226 expect(rows).toHaveLength(2);269 source_id: "a",
227 expect(rows).toEqual([270 name: "b:c",
228 {271 text_value: "second",
229 id: '["a","b:c"]',272 },
230 source_id: "a",273 {
231 name: "b:c",274 id: '["a:b","c"]',
232 text_value: "second",275 source_id: "a:b",
233 },276 name: "c",
234 {277 text_value: "first",
235 id: '["a:b","c"]',278 },
236 source_id: "a:b",279 ]);
237 name: "c",280 after.close();
238 text_value: "first",
239 },
240 ]);
241 after.close();
242 } finally {
243 rmSync(dir, { recursive: true, force: true });
244 }
245 });281 });
246});282});