chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,493 @@
|
||||
import { createTestHttpServer } from "@epic-web/test-server/http";
|
||||
import {
|
||||
replaceSuperJsonPayload,
|
||||
prettyPrintPacket,
|
||||
conditionallyExportPacket,
|
||||
type IOPacket,
|
||||
} from "../src/v3/utils/ioSerialization.js";
|
||||
import { ApiClient } from "../src/v3/apiClient/index.js";
|
||||
import { apiClientManager } from "../src/v3/apiClientManager-api.js";
|
||||
|
||||
describe("ioSerialization", () => {
|
||||
describe("replaceSuperJsonPayload", () => {
|
||||
it("should replace simple JSON payload while preserving SuperJSON metadata", async () => {
|
||||
const originalData = {
|
||||
name: "John",
|
||||
age: 30,
|
||||
date: new Date("2023-01-01"),
|
||||
};
|
||||
|
||||
const superjson = await import("superjson");
|
||||
const originalSerialized = superjson.stringify(originalData);
|
||||
|
||||
const newPayloadJson = JSON.stringify({
|
||||
name: "Jane",
|
||||
surname: "Doe",
|
||||
age: 25,
|
||||
date: "2023-02-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
const result = (await replaceSuperJsonPayload(originalSerialized, newPayloadJson)) as any;
|
||||
|
||||
expect(result.name).toBe("Jane");
|
||||
expect(result.surname).toBe("Doe");
|
||||
expect(result.age).toBe(25);
|
||||
expect(result.date).toBeInstanceOf(Date);
|
||||
expect(result.date.toISOString()).toBe("2023-02-01T00:00:00.000Z");
|
||||
});
|
||||
|
||||
// related to issue https://github.com/triggerdotdev/trigger.dev/issues/1968
|
||||
it("should ignore original undefined type metadata for overriden fields", async () => {
|
||||
const originalData = {
|
||||
name: "John",
|
||||
age: 30,
|
||||
date: new Date("2023-01-01"),
|
||||
country: undefined,
|
||||
settings: {
|
||||
theme: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
const superjson = await import("superjson");
|
||||
const originalSerialized = superjson.stringify(originalData);
|
||||
|
||||
const newPayloadJson = JSON.stringify({
|
||||
name: "Jane",
|
||||
surname: "Doe",
|
||||
age: 25,
|
||||
date: "2023-02-01T00:00:00.000Z",
|
||||
country: "US",
|
||||
settings: {
|
||||
theme: "dark",
|
||||
},
|
||||
});
|
||||
|
||||
const result = (await replaceSuperJsonPayload(originalSerialized, newPayloadJson)) as any;
|
||||
|
||||
expect(result.name).toBe("Jane");
|
||||
expect(result.surname).toBe("Doe");
|
||||
expect(result.country).toBe("US");
|
||||
expect(result.settings.theme).toBe("dark");
|
||||
expect(result.age).toBe(25);
|
||||
expect(result.date).toBeInstanceOf(Date);
|
||||
expect(result.date.toISOString()).toBe("2023-02-01T00:00:00.000Z");
|
||||
});
|
||||
|
||||
it("should preserve BigInt type metadata", async () => {
|
||||
const originalData = {
|
||||
id: BigInt(123456789),
|
||||
count: 42,
|
||||
};
|
||||
|
||||
const superjson = await import("superjson");
|
||||
const originalSerialized = superjson.stringify(originalData);
|
||||
|
||||
const newPayloadJson = JSON.stringify({
|
||||
id: "987654321",
|
||||
count: 100,
|
||||
});
|
||||
|
||||
const result = (await replaceSuperJsonPayload(originalSerialized, newPayloadJson)) as any;
|
||||
|
||||
expect(result.id).toBe(BigInt(987654321));
|
||||
expect(typeof result.id).toBe("bigint");
|
||||
expect(result.count).toBe(100);
|
||||
});
|
||||
|
||||
it("should preserve nested type metadata", async () => {
|
||||
const originalData = {
|
||||
user: {
|
||||
id: BigInt(123),
|
||||
createdAt: new Date("2023-01-01"),
|
||||
settings: {
|
||||
theme: "dark",
|
||||
updatedAt: new Date("2023-01-01"),
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const superjson = await import("superjson");
|
||||
const originalSerialized = superjson.stringify(originalData);
|
||||
|
||||
const newPayloadJson = JSON.stringify({
|
||||
user: {
|
||||
id: "456",
|
||||
createdAt: "2023-06-01T00:00:00.000Z",
|
||||
settings: {
|
||||
theme: "light",
|
||||
updatedAt: "2023-06-01T00:00:00.000Z",
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
version: 2,
|
||||
},
|
||||
});
|
||||
|
||||
const result = (await replaceSuperJsonPayload(originalSerialized, newPayloadJson)) as any;
|
||||
|
||||
expect(result.user.id).toBe(BigInt(456));
|
||||
expect(result.user.createdAt).toBeInstanceOf(Date);
|
||||
expect(result.user.createdAt.toISOString()).toBe("2023-06-01T00:00:00.000Z");
|
||||
expect(result.user.settings.theme).toBe("light");
|
||||
expect(result.user.settings.updatedAt).toBeInstanceOf(Date);
|
||||
expect(result.user.settings.updatedAt.toISOString()).toBe("2023-06-01T00:00:00.000Z");
|
||||
expect(result.metadata.version).toBe(2);
|
||||
});
|
||||
|
||||
it("should preserve Set type metadata", async () => {
|
||||
const originalData = {
|
||||
tags: new Set(["tag1", "tag2"]),
|
||||
name: "test",
|
||||
};
|
||||
|
||||
const superjson = await import("superjson");
|
||||
const originalSerialized = superjson.stringify(originalData);
|
||||
|
||||
const newPayloadJson = JSON.stringify({
|
||||
tags: ["tag3", "tag4", "tag5"],
|
||||
name: "updated",
|
||||
});
|
||||
|
||||
const result = (await replaceSuperJsonPayload(originalSerialized, newPayloadJson)) as any;
|
||||
|
||||
expect(result.tags).toBeInstanceOf(Set);
|
||||
expect(Array.from(result.tags)).toEqual(["tag3", "tag4", "tag5"]);
|
||||
expect(result.name).toBe("updated");
|
||||
});
|
||||
|
||||
it("should preserve Map type metadata", async () => {
|
||||
const originalData = {
|
||||
mapping: new Map([
|
||||
["key1", "value1"],
|
||||
["key2", "value2"],
|
||||
]),
|
||||
name: "test",
|
||||
};
|
||||
|
||||
const superjson = await import("superjson");
|
||||
const originalSerialized = superjson.stringify(originalData);
|
||||
|
||||
const newPayloadJson = JSON.stringify({
|
||||
mapping: [
|
||||
["key3", "value3"],
|
||||
["key4", "value4"],
|
||||
],
|
||||
name: "updated",
|
||||
});
|
||||
|
||||
const result = (await replaceSuperJsonPayload(originalSerialized, newPayloadJson)) as any;
|
||||
|
||||
expect(result.mapping).toBeInstanceOf(Map);
|
||||
expect(result.mapping.get("key3")).toBe("value3");
|
||||
expect(result.mapping.get("key4")).toBe("value4");
|
||||
expect(result.name).toBe("updated");
|
||||
});
|
||||
|
||||
it("should throw error for invalid JSON payload", async () => {
|
||||
const originalData = { name: "test" };
|
||||
|
||||
const superjson = await import("superjson");
|
||||
const originalSerialized = superjson.stringify(originalData);
|
||||
const invalidPayload = "{ invalid json }";
|
||||
|
||||
await expect(replaceSuperJsonPayload(originalSerialized, invalidPayload)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("prettyPrintPacket", () => {
|
||||
it("should return empty string for undefined data", async () => {
|
||||
const result = await prettyPrintPacket(undefined);
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("should return string data as-is", async () => {
|
||||
const result = await prettyPrintPacket("Hello, World!");
|
||||
expect(result).toBe("Hello, World!");
|
||||
});
|
||||
|
||||
it("should pretty print JSON data with default options", async () => {
|
||||
const data = { name: "John", age: 30, nested: { value: true } };
|
||||
const result = await prettyPrintPacket(data, "application/json");
|
||||
|
||||
expect(result).toBe(JSON.stringify(data, null, 2));
|
||||
});
|
||||
|
||||
it("should handle JSON data as string", async () => {
|
||||
const data = { name: "John", age: 30 };
|
||||
const jsonString = JSON.stringify(data);
|
||||
const result = await prettyPrintPacket(jsonString, "application/json");
|
||||
|
||||
expect(result).toBe(JSON.stringify(data, null, 2));
|
||||
});
|
||||
|
||||
it("should pretty print SuperJSON data", async () => {
|
||||
const data = {
|
||||
name: "John",
|
||||
date: new Date("2023-01-01"),
|
||||
bigInt: BigInt(123),
|
||||
set: new Set(["a", "b"]),
|
||||
map: new Map([["key", "value"]]),
|
||||
};
|
||||
|
||||
const superjson = await import("superjson");
|
||||
const serialized = superjson.stringify(data);
|
||||
|
||||
const result = await prettyPrintPacket(serialized, "application/super+json");
|
||||
|
||||
// Should deserialize and pretty print the data
|
||||
expect(result).toContain('"name": "John"');
|
||||
expect(result).toContain('"date": "2023-01-01T00:00:00.000Z"');
|
||||
expect(result).toContain('"bigInt": "123"');
|
||||
expect(result).toContain('"set": [\n "a",\n "b"\n ]');
|
||||
expect(result).toContain('"map": {\n "key": "value"\n }');
|
||||
});
|
||||
|
||||
it("should handle circular references", async () => {
|
||||
const data: any = { name: "John" };
|
||||
data.self = data; // Create circular reference
|
||||
|
||||
// Create a SuperJSON serialized version to test the circular reference detection
|
||||
const superjson = await import("superjson");
|
||||
const serialized = superjson.stringify(data);
|
||||
|
||||
const result = await prettyPrintPacket(serialized, "application/super+json");
|
||||
|
||||
expect(result).toContain('"name": "John"');
|
||||
expect(result).toContain('"self": "[Circular]"');
|
||||
});
|
||||
|
||||
it("should handle regular non-circular references", async () => {
|
||||
const person = { name: "John" };
|
||||
|
||||
const data: any = { person1: person, person2: person };
|
||||
|
||||
// Create a SuperJSON serialized version to test the circular reference detection
|
||||
const superjson = await import("superjson");
|
||||
const serialized = superjson.stringify(data);
|
||||
|
||||
const result = await prettyPrintPacket(serialized, "application/super+json");
|
||||
|
||||
expect(result).toContain('"person1": {');
|
||||
expect(result).toContain('"person2": {');
|
||||
});
|
||||
|
||||
it("should filter out specified keys", async () => {
|
||||
const data = { name: "John", password: "secret", age: 30 };
|
||||
const result = await prettyPrintPacket(data, "application/json", {
|
||||
filteredKeys: ["password"],
|
||||
});
|
||||
|
||||
expect(result).toContain('"name": "John"');
|
||||
expect(result).toContain('"age": 30');
|
||||
expect(result).not.toContain('"password"');
|
||||
});
|
||||
|
||||
it("should handle BigInt values", async () => {
|
||||
const data = { id: BigInt(123456789), name: "John" };
|
||||
const result = await prettyPrintPacket(data, "application/json");
|
||||
|
||||
expect(result).toContain('"id": "123456789"');
|
||||
expect(result).toContain('"name": "John"');
|
||||
});
|
||||
|
||||
it("should handle RegExp values", async () => {
|
||||
const data = { pattern: /test/gi, name: "John" };
|
||||
const result = await prettyPrintPacket(data, "application/json");
|
||||
|
||||
expect(result).toContain('"pattern": "/test/gi"');
|
||||
expect(result).toContain('"name": "John"');
|
||||
});
|
||||
|
||||
it("should handle Set values", async () => {
|
||||
const data = { tags: new Set(["tag1", "tag2"]), name: "John" };
|
||||
const result = await prettyPrintPacket(data, "application/json");
|
||||
|
||||
expect(result).toContain('"tags": [\n "tag1",\n "tag2"\n ]');
|
||||
expect(result).toContain('"name": "John"');
|
||||
});
|
||||
|
||||
it("should handle Map values", async () => {
|
||||
const data = { mapping: new Map([["key1", "value1"]]), name: "John" };
|
||||
const result = await prettyPrintPacket(data, "application/json");
|
||||
|
||||
expect(result).toContain('"mapping": {\n "key1": "value1"\n }');
|
||||
expect(result).toContain('"name": "John"');
|
||||
});
|
||||
|
||||
it("should handle complex nested data", async () => {
|
||||
const data = {
|
||||
user: {
|
||||
id: BigInt(123),
|
||||
createdAt: new Date("2023-01-01"),
|
||||
settings: {
|
||||
theme: "dark",
|
||||
tags: new Set(["admin", "user"]),
|
||||
config: new Map([["timeout", "30s"]]),
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
version: 1,
|
||||
pattern: /^test$/,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await prettyPrintPacket(data, "application/json");
|
||||
|
||||
expect(result).toContain('"id": "123"');
|
||||
expect(result).toContain('"createdAt": "2023-01-01T00:00:00.000Z"');
|
||||
expect(result).toContain('"theme": "dark"');
|
||||
expect(result).toContain('"tags": [\n "admin",\n "user"\n ]');
|
||||
expect(result).toContain('"config": {\n "timeout": "30s"\n }');
|
||||
expect(result).toContain('"version": 1');
|
||||
expect(result).toContain('"pattern": "/^test$/"');
|
||||
});
|
||||
|
||||
it("should handle data without dataType parameter", async () => {
|
||||
const data = { name: "John", age: 30 };
|
||||
const result = await prettyPrintPacket(data);
|
||||
|
||||
expect(result).toBe(JSON.stringify(data, null, 2));
|
||||
});
|
||||
});
|
||||
|
||||
describe("conditionallyExportPacket", () => {
|
||||
// A payload large enough to exceed OFFLOAD_IO_PACKET_LENGTH_LIMIT (128KB) so it offloads.
|
||||
const largePayload = "x".repeat(200_000);
|
||||
const largePacket: IOPacket = { data: largePayload, dataType: "text/plain" };
|
||||
|
||||
afterEach(() => {
|
||||
// Clear any global client config set during a test.
|
||||
apiClientManager.disable();
|
||||
});
|
||||
|
||||
it("uses the provided client for the upload presign instead of the global client", async () => {
|
||||
const globalPresignRequests: string[] = [];
|
||||
const passedPresignRequests: string[] = [];
|
||||
let uploadedBytes = 0;
|
||||
|
||||
// The global client points here — it must NOT be hit when a client is passed.
|
||||
const globalServer = await createTestHttpServer({
|
||||
defineRoutes(router) {
|
||||
router.put("/api/v2/packets/:filename", async ({ req }) => {
|
||||
globalPresignRequests.push(req.url);
|
||||
return Response.json({ presignedUrl: "http://unused.local/upload" });
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// The explicitly passed client points here — it MUST receive the presign + upload.
|
||||
const passedServer = await createTestHttpServer({
|
||||
defineRoutes(router) {
|
||||
router.put("/api/v2/packets/:filename", async ({ req }) => {
|
||||
passedPresignRequests.push(req.url);
|
||||
return Response.json({
|
||||
presignedUrl: `${passedServer.http.url().origin}/upload/payload`,
|
||||
storagePath: "trigger/task/payload.txt",
|
||||
});
|
||||
});
|
||||
router.put("/upload/payload", async ({ req }) => {
|
||||
uploadedBytes = (await req.text()).length;
|
||||
return new Response(null, { status: 200 });
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Configure the global client to point at the global server.
|
||||
apiClientManager.setGlobalAPIClientConfiguration({
|
||||
baseURL: globalServer.http.url().origin,
|
||||
accessToken: "tr-global",
|
||||
});
|
||||
|
||||
const passedClient = new ApiClient(passedServer.http.url().origin, "tr-passed");
|
||||
|
||||
const result = await conditionallyExportPacket(
|
||||
largePacket,
|
||||
"trigger/task/payload",
|
||||
undefined,
|
||||
passedClient
|
||||
);
|
||||
|
||||
expect(result.dataType).toBe("application/store");
|
||||
expect(result.data).toBe("trigger/task/payload.txt");
|
||||
|
||||
// Upload went through the passed client only.
|
||||
expect(passedPresignRequests).toHaveLength(1);
|
||||
expect(globalPresignRequests).toHaveLength(0);
|
||||
expect(uploadedBytes).toBe(largePayload.length);
|
||||
} finally {
|
||||
await globalServer.close();
|
||||
await passedServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to the global client when no client is provided", async () => {
|
||||
const presignRequests: string[] = [];
|
||||
|
||||
const globalServer = await createTestHttpServer({
|
||||
defineRoutes(router) {
|
||||
router.put("/api/v2/packets/:filename", async ({ req }) => {
|
||||
presignRequests.push(req.url);
|
||||
return Response.json({
|
||||
presignedUrl: `${globalServer.http.url().origin}/upload/payload`,
|
||||
storagePath: "trigger/task/payload.txt",
|
||||
});
|
||||
});
|
||||
router.put("/upload/payload", async () => new Response(null, { status: 200 }));
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
apiClientManager.setGlobalAPIClientConfiguration({
|
||||
baseURL: globalServer.http.url().origin,
|
||||
accessToken: "tr-global",
|
||||
});
|
||||
|
||||
const result = await conditionallyExportPacket(largePacket, "trigger/task/payload");
|
||||
|
||||
expect(result.dataType).toBe("application/store");
|
||||
expect(result.data).toBe("trigger/task/payload.txt");
|
||||
expect(presignRequests).toHaveLength(1);
|
||||
} finally {
|
||||
await globalServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("returns the packet unchanged when no client is available", async () => {
|
||||
apiClientManager.disable();
|
||||
|
||||
const result = await conditionallyExportPacket(largePacket, "trigger/task/payload");
|
||||
|
||||
expect(result).toEqual(largePacket);
|
||||
});
|
||||
|
||||
it("does not offload small payloads even with a client", async () => {
|
||||
const passedServer = await createTestHttpServer({
|
||||
defineRoutes(router) {
|
||||
router.put("/api/v2/packets/:filename", async () => {
|
||||
throw new Error("presign should not be called for small payloads");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const smallPacket: IOPacket = { data: "hello", dataType: "text/plain" };
|
||||
const passedClient = new ApiClient(passedServer.http.url().origin, "tr-passed");
|
||||
|
||||
const result = await conditionallyExportPacket(
|
||||
smallPacket,
|
||||
"trigger/task/payload",
|
||||
undefined,
|
||||
passedClient
|
||||
);
|
||||
|
||||
expect(result).toEqual(smallPacket);
|
||||
} finally {
|
||||
await passedServer.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user