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 @@
1/* oxlint-disable executor/no-error-constructor, executor/no-try-catch-or-throw -- boundary: one-shot migration CLI */1/* oxlint-disable executor/no-error-constructor, executor/no-try-catch-or-throw -- boundary: one-shot migration CLI */
2 2
3import { createHash } from "node:crypto";
3import { mkdirSync, readFileSync, writeFileSync } from "node:fs";4import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4import { resolve } from "node:path";5import { resolve } from "node:path";
5import { pathToFileURL } from "node:url";6import { pathToFileURL } from "node:url";
@@ -220,14 +221,10 @@ const scrubJson = (value: unknown): unknown => { · 214 unmodified lines
220 return value;221 return value;
221};222};
222 223
223const stableId = (...parts: readonly string[]): string => {224// sha256-based: a 31-bit hash collides around ~50k rows (observed in prod);
224 const text = parts.join("\u0000");225// 96 bits keeps re-run determinism with no realistic collision risk.
225 let hash = 0;226const stableId = (...parts: readonly string[]): string =>
226 for (let index = 0; index < text.length; index += 1) {227 `service_split_${createHash("sha256").update(parts.join("\u0000")).digest("hex").slice(0, 24)}`;
227 hash = (hash * 31 + text.charCodeAt(index)) >>> 0;
228 }
229 return `service_split_${hash.toString(36)}`;
230};
231 228
232const operationKeyPrefix = (integration: string): string =>229const operationKeyPrefix = (integration: string): string =>
233 `${operationStorageKey(integration, "").split(".").slice(0, 2).join(".")}.%`;230 `${operationStorageKey(integration, "").split(".").slice(0, 2).join(".")}.%`;
@@ -280,8 +277,8 @@ export const applyOrg = async (sql: SqlClient, org: OrgPlan): Promise<void> => { · 46 unmodified lines
280 row.target.pluginId,277 row.target.pluginId,
281 row.target.name,278 row.target.name,
282 row.target.description,279 row.target.description,
283 JSON.stringify(scrubJson(row.config)),280 scrubJson(row.config),
284 row.healthCheck ? JSON.stringify(scrubJson(row.healthCheck)) : null,281 row.healthCheck ? scrubJson(row.healthCheck) : null,
285 now,282 now,
286 stableId("integration", org.tenant, row.target.slug),283 stableId("integration", org.tenant, row.target.slug),
287 org.tenant,284 org.tenant,
@@ -391,13 +388,11 @@ export const applyOrg = async (sql: SqlClient, org: OrgPlan): Promise<void> => { · 103 unmodified lines
391 params.push(388 params.push(
392 integration.target.pluginId,389 integration.target.pluginId,
393 operationStorageKey(integration.target.slug, toolName),390 operationStorageKey(integration.target.slug, toolName),
394 JSON.stringify(391 scrubJson({
395 scrubJson({392 ...storageDataRecord(operation),
396 ...storageDataRecord(operation),393 integration: integration.target.slug,
397 integration: integration.target.slug,394 toolName,
398 toolName,395 }),
399 }),
400 ),
401 operation.created_at,396 operation.created_at,
402 now,397 now,
403 stableId("operation", org.tenant, integration.target.slug, toolName),398 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
363 readonly plugin_id: string;363 readonly plugin_id: string;
364 readonly config: unknown;364 readonly config: unknown;
365 }[]365 }[]
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 );
367 expect(integrations).toHaveLength(1);369 expect(integrations).toHaveLength(1);
368 expect(integrations[0]).toMatchObject({370 expect(integrations[0]).toMatchObject({
369 slug: "google_calendar",371 slug: "google_calendar",
370 plugin_id: "openapi",372 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",
371 });376 });
372 expect(parseJson(integrations[0]?.config)).toMatchObject({377 expect(parseJson(integrations[0]?.config)).toMatchObject({
373 specHash: "mono-hash",378 specHash: "mono-hash",
@@ -401,11 +406,14 @@ describe("service-split-postgres-cli", () => { · 27 unmodified lines
401 readonly key: string;406 readonly key: string;
402 readonly data: unknown;407 readonly data: unknown;
403 }[]408 }[]
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 );
405 expect(operations).toHaveLength(1);412 expect(operations).toHaveLength(1);
406 expect(operations[0]).toMatchObject({413 expect(operations[0]).toMatchObject({
407 plugin_id: "openapi",414 plugin_id: "openapi",
408 key: operationStorageKey("google_calendar", "calendar.events.list"),415 key: operationStorageKey("google_calendar", "calendar.events.list"),
416 data_type: "object",
409 });417 });
410 expect(parseJson(operations[0]?.data)).toMatchObject({418 expect(parseJson(operations[0]?.data)).toMatchObject({
411 integration: "google_calendar",419 integration: "google_calendar",