History

Write migration JSON params as objects and widen row ids

Rhys Sullivan committed 4 months ago

Commit 9cc69e41 parent 77756bb2 files · +22 −19Browse files

Unified · Split

2 files
  1. scripts/migration/service-split-postgres-cli.ts +12 −17
  2. tests/migration/service-split-postgres-cli.test.ts +10 −2
scripts/migration/service-split-postgres-cli.ts+12−17
@@ -1,5 +1,6 @@
11 /* oxlint-disable executor/no-error-constructor, executor/no-try-catch-or-throw -- boundary: one-shot migration CLI */
22
3+import { createHash } from "node:crypto";
34 import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
45 import { resolve } from "node:path";
56 import { pathToFileURL } from "node:url";
@@ -220,14 +221,10 @@ const scrubJson = (value: unknown): unknown => { · 214 unmodified lines
220221 return value;
221222 };
222223
223−const stableId = (...parts: readonly string[]): string => {
224− const text = parts.join("\u0000");
225− let hash = 0;
226− for (let index = 0; index < text.length; index += 1) {
227− hash = (hash * 31 + text.charCodeAt(index)) >>> 0;
228− }
229− return `service_split_${hash.toString(36)}`;
230−};
224+// sha256-based: a 31-bit hash collides around ~50k rows (observed in prod);
225+// 96 bits keeps re-run determinism with no realistic collision risk.
226+const stableId = (...parts: readonly string[]): string =>
227+ `service_split_${createHash("sha256").update(parts.join("\u0000")).digest("hex").slice(0, 24)}`;
231228
232229 const operationKeyPrefix = (integration: string): string =>
233230 `${operationStorageKey(integration, "").split(".").slice(0, 2).join(".")}.%`;
@@ -280,8 +277,8 @@ export const applyOrg = async (sql: SqlClient, org: OrgPlan): Promise<void> => { · 46 unmodified lines
280277 row.target.pluginId,
281278 row.target.name,
282279 row.target.description,
283− JSON.stringify(scrubJson(row.config)),
284− row.healthCheck ? JSON.stringify(scrubJson(row.healthCheck)) : null,
280+ scrubJson(row.config),
281+ row.healthCheck ? scrubJson(row.healthCheck) : null,
285282 now,
286283 stableId("integration", org.tenant, row.target.slug),
287284 org.tenant,
@@ -391,13 +388,11 @@ export const applyOrg = async (sql: SqlClient, org: OrgPlan): Promise<void> => { · 103 unmodified lines
391388 params.push(
392389 integration.target.pluginId,
393390 operationStorageKey(integration.target.slug, toolName),
394− JSON.stringify(
395− scrubJson({
396− ...storageDataRecord(operation),
397− integration: integration.target.slug,
398− toolName,
399− }),
400− ),
391+ scrubJson({
392+ ...storageDataRecord(operation),
393+ integration: integration.target.slug,
394+ toolName,
395+ }),
401396 operation.created_at,
402397 now,
403398 stableId("operation", org.tenant, integration.target.slug, toolName),
tests/migration/service-split-postgres-cli.test.ts+10−2
@@ -363,11 +363,16 @@ describe("service-split-postgres-cli", () => { · 362 unmodified lines
363363 readonly plugin_id: string;
364364 readonly config: unknown;
365365 }[]
366− >("SELECT slug, plugin_id, config::jsonb AS config FROM integration ORDER BY slug");
366+ >(
367+ "SELECT slug, plugin_id, config::jsonb AS config, jsonb_typeof(config::jsonb) AS config_type FROM integration ORDER BY slug",
368+ );
367369 expect(integrations).toHaveLength(1);
368370 expect(integrations[0]).toMatchObject({
369371 slug: "google_calendar",
370372 plugin_id: "openapi",
373+ // A double-encoded config lands as jsonb string, which some runtime
374+ // readers reject; the migration must write a real JSON object.
375+ config_type: "object",
371376 });
372377 expect(parseJson(integrations[0]?.config)).toMatchObject({
373378 specHash: "mono-hash",
@@ -401,11 +406,14 @@ describe("service-split-postgres-cli", () => { · 27 unmodified lines
401406 readonly key: string;
402407 readonly data: unknown;
403408 }[]
404− >("SELECT plugin_id, key, data FROM plugin_storage WHERE collection = 'operation'");
409+ >(
410+ "SELECT plugin_id, key, data, jsonb_typeof(data::jsonb) AS data_type FROM plugin_storage WHERE collection = 'operation'",
411+ );
405412 expect(operations).toHaveLength(1);
406413 expect(operations[0]).toMatchObject({
407414 plugin_id: "openapi",
408415 key: operationStorageKey("google_calendar", "calendar.events.list"),
416+ data_type: "object",
409417 });
410418 expect(parseJson(operations[0]?.data)).toMatchObject({
411419 integration: "google_calendar",