Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 33 additions & 57 deletions src/core/functions/uploads/fileArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,52 +126,41 @@ export const uploadFileArray = async (
data.append("file", file, path);
}

// Reserved for later release
// data.append("name", folder);

// data.append("network", network);

// if (options?.groupId) {
// data.append("group_id", options.groupId);
// }

// if (options?.metadata?.keyvalues) {
// data.append("keyvalues", JSON.stringify(options.metadata.keyvalues));
// }

// Legacy
data.append(
"pinataMetadata",
JSON.stringify({
name: folder,
keyvalues: options?.metadata?.keyvalues,
}),
);

data.append(
"pinataOptions",
JSON.stringify({
groupId: options?.groupId,
cidVersion: 1,
}),
);

// Reserved for later release
//let endpoint: string = "https://uploads.pinata.cloud/v3";
let endpoint: string = "https://api.pinata.cloud/pinning/pinFileToIPFS";

if (config.legacyUploadUrl) {
endpoint = config.legacyUploadUrl;
data.append("network", network);
data.append("name", folder);

if (options?.groupId) {
data.append("group_id", options.groupId);
}

if (options?.metadata?.keyvalues) {
data.append("keyvalues", JSON.stringify(options.metadata.keyvalues));
}

if (options?.streamable) {
data.append("streamable", "true");
}

if (options?.car) {
data.append("car", "true");
}

if (options?.cid_version !== undefined) {
data.append("cid_version", options.cid_version.toString());
}

if (options?.expires_at !== undefined) {
data.append("expires_at", options.expires_at.toString());
}

let endpoint: string = "https://uploads.pinata.cloud/v3";

if (config.uploadUrl) {
endpoint = config.uploadUrl;
}

try {
// Reserved for later release
// const request = await fetch(`${endpoint}/files`, {
// method: "POST",
// headers: headers,
// body: data,
// });
const request = await fetch(`${endpoint}`, {
const request = await fetch(`${endpoint}/files`, {
method: "POST",
headers: headers,
body: data,
Expand Down Expand Up @@ -202,20 +191,7 @@ export const uploadFileArray = async (
}

const res = await request.json();

const resData: UploadResponse = {
id: res.ID,
name: res.Name,
cid: res.IpfsHash,
size: res.PinSize,
created_at: res.Timestamp,
number_of_files: res.NumberOfFiles,
mime_type: res.MimeType,
group_id: res.GroupId,
keyvalues: res.Keyvalues,
vectorized: false,
network: "public",
};
const resData: UploadResponse = res.data;

// if (options?.vectorize) {
// const vectorReq = await fetch(
Expand Down
146 changes: 104 additions & 42 deletions tests/uploads/fileArray.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,7 @@ describe("uploadFileArray function", () => {
new File(["two"], "b.txt", { type: "text/plain" }),
];

// Legacy pinFileToIPFS shape (PascalCase keys)
const mockLegacyResponse = {
ID: "legacy-id",
Name: "folder_from_sdk",
IpfsHash: "QmLegacyDirectory",
PinSize: 246,
Timestamp: "2023-01-01T00:00:00Z",
NumberOfFiles: 2,
MimeType: "directory",
GroupId: null,
Keyvalues: {},
};

// v3 envelope-style response (returned by signed-URL endpoint)
// v3 envelope-style response shape returned by uploads.pinata.cloud/v3/files
const mockV3Response: UploadResponse = {
id: "v3-id",
name: "folder_from_sdk",
Expand All @@ -68,31 +55,18 @@ describe("uploadFileArray function", () => {
).rejects.toThrow(ValidationError);
});

describe("legacy pinFileToIPFS path", () => {
it("should POST to pinFileToIPFS and remap response", async () => {
describe("default v3 path", () => {
it("should POST to uploads.pinata.cloud/v3/files and unwrap res.data", async () => {
global.fetch = jest.fn().mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValueOnce(mockLegacyResponse),
json: jest.fn().mockResolvedValueOnce({ data: mockV3Response }),
});

const result = await uploadFileArray(mockConfig, mockFiles, "public");

expect(result).toEqual({
id: "legacy-id",
name: "folder_from_sdk",
cid: "QmLegacyDirectory",
size: 246,
created_at: "2023-01-01T00:00:00Z",
number_of_files: 2,
mime_type: "directory",
group_id: null,
keyvalues: {},
vectorized: false,
network: "public",
});

expect(result).toEqual(mockV3Response);
expect(global.fetch).toHaveBeenCalledWith(
"https://api.pinata.cloud/pinning/pinFileToIPFS",
"https://uploads.pinata.cloud/v3/files",
expect.objectContaining({
method: "POST",
headers: {
Expand All @@ -102,36 +76,124 @@ describe("uploadFileArray function", () => {
body: expect.any(FormData),
}),
);
});

it("should send the v3 directory shape and not legacy pinata* fields", async () => {
global.fetch = jest.fn().mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValueOnce({ data: mockV3Response }),
});

await uploadFileArray(mockConfig, mockFiles, "public", {
groupId: "group-1",
metadata: {
name: "my-folder",
keyvalues: { env: "prod" },
},
cid_version: "v1" as CidVersion,
expires_at: 1735689600,
streamable: true,
car: true,
});

const fetchCall = (global.fetch as jest.Mock).mock.calls[0];
const formData = fetchCall[1].body as FormData;
const fileEntries = formData.getAll("file");
expect(fileEntries).toHaveLength(2);

const metadata = JSON.parse(formData.get("pinataMetadata") as string);
expect(metadata.name).toBe("folder_from_sdk");
expect(formData.get("name")).toBe("my-folder");
expect(formData.get("network")).toBe("public");
expect(formData.get("group_id")).toBe("group-1");
expect(formData.get("keyvalues")).toBe(JSON.stringify({ env: "prod" }));
expect(formData.get("cid_version")).toBe("v1");
expect(formData.get("expires_at")).toBe("1735689600");
expect(formData.get("streamable")).toBe("true");
expect(formData.get("car")).toBe("true");

expect(formData.get("pinataMetadata")).toBeNull();
expect(formData.get("pinataOptions")).toBeNull();
});

it("should never hit the legacy pinFileToIPFS endpoint", async () => {
global.fetch = jest.fn().mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValueOnce({ data: mockV3Response }),
});

await uploadFileArray(mockConfig, mockFiles, "public");

const opts = JSON.parse(formData.get("pinataOptions") as string);
expect(opts.cidVersion).toBe(1);
const calls = (global.fetch as jest.Mock).mock.calls;
for (const call of calls) {
expect(call[0]).not.toContain("api.pinata.cloud/pinning/pinFileToIPFS");
}
});

it("should respect a custom legacyUploadUrl", async () => {
it("should respect a custom config.uploadUrl", async () => {
global.fetch = jest.fn().mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValueOnce(mockLegacyResponse),
json: jest.fn().mockResolvedValueOnce({ data: mockV3Response }),
});

await uploadFileArray(
{ ...mockConfig, legacyUploadUrl: "https://custom.example/legacy" },
{ ...mockConfig, uploadUrl: "https://custom.example/v3" },
mockFiles,
"public",
);

expect(global.fetch).toHaveBeenCalledWith(
"https://custom.example/legacy",
"https://custom.example/v3/files",
expect.any(Object),
);
});

it("should pass network=private through to FormData", async () => {
global.fetch = jest.fn().mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValueOnce({
data: { ...mockV3Response, network: "private" },
}),
});

await uploadFileArray(mockConfig, mockFiles, "private");

const fetchCall = (global.fetch as jest.Mock).mock.calls[0];
const formData = fetchCall[1].body as FormData;
expect(formData.get("network")).toBe("private");
});

it("should throw AuthenticationError on 401", async () => {
global.fetch = jest.fn().mockResolvedValueOnce({
ok: false,
status: 401,
url: "https://uploads.pinata.cloud/v3/files",
text: jest.fn().mockResolvedValueOnce("Unauthorized"),
});

await expect(
uploadFileArray(mockConfig, mockFiles, "public"),
).rejects.toThrow(AuthenticationError);
});

it("should throw NetworkError on non-auth error response", async () => {
global.fetch = jest.fn().mockResolvedValueOnce({
ok: false,
status: 500,
url: "https://uploads.pinata.cloud/v3/files",
text: jest.fn().mockResolvedValueOnce("Server Error"),
});

await expect(
uploadFileArray(mockConfig, mockFiles, "public"),
).rejects.toThrow(NetworkError);
});

it("should throw PinataError when fetch itself rejects", async () => {
global.fetch = jest
.fn()
.mockRejectedValueOnce(new Error("Network failure"));

await expect(
uploadFileArray(mockConfig, mockFiles, "public"),
).rejects.toThrow(PinataError);
});
});

describe("signed URL path (options.url)", () => {
Expand Down
Loading