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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@credal/sdk",
"version": "0.1.9",
"version": "0.1.10",
"private": false,
"repository": "github:credal-ai/credal-typescript-sdk",
"type": "commonjs",
Expand Down
4 changes: 2 additions & 2 deletions src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export class CredalClient {
{
"X-Fern-Language": "JavaScript",
"X-Fern-SDK-Name": "@credal/sdk",
"X-Fern-SDK-Version": "0.1.9",
"User-Agent": "@credal/sdk/0.1.9",
"X-Fern-SDK-Version": "0.1.10",
"User-Agent": "@credal/sdk/0.1.10",
"X-Fern-Runtime": core.RUNTIME.type,
"X-Fern-Runtime-Version": core.RUNTIME.version,
},
Expand Down
112 changes: 112 additions & 0 deletions src/api/resources/documentCatalog/client/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,118 @@ export class DocumentCatalog {
}
}

/**
* Upload a file (PDF, Word, Excel, CSV, PowerPoint) to Credal. Unlike uploadDocumentContents which requires pre-parsed text, this endpoint accepts actual file uploads and automatically parses them using Credal's parsing service.
*
* @param {Credal.UploadFileRequest} request
* @param {DocumentCatalog.RequestOptions} requestOptions - Request-specific configuration.
*/
public uploadFile(
request: Credal.UploadFileRequest,
requestOptions?: DocumentCatalog.RequestOptions,
): core.HttpResponsePromise<Credal.UploadDocumentResponse> {
return core.HttpResponsePromise.fromPromise(this.__uploadFile(request, requestOptions));
}

private async __uploadFile(
request: Credal.UploadFileRequest,
requestOptions?: DocumentCatalog.RequestOptions,
): Promise<core.WithRawResponse<Credal.UploadDocumentResponse>> {
const _request = await core.newFormData();
await _request.appendFile("file", request.file);
if (request.documentName != null) {
_request.append("documentName", request.documentName);
}

_request.append("uploadAsUserEmail", request.uploadAsUserEmail);
_request.append("documentExternalId", request.documentExternalId);
if (request.allowedUsersEmailAddresses != null) {
_request.append("allowedUsersEmailAddresses", request.allowedUsersEmailAddresses);
}

if (request.documentExternalUrl != null) {
_request.append("documentExternalUrl", request.documentExternalUrl);
}

if (request.customMetadata != null) {
_request.append("customMetadata", request.customMetadata);
}

if (request.collectionId != null) {
_request.append("collectionId", request.collectionId);
}

if (request.forceUpdate != null) {
_request.append("forceUpdate", request.forceUpdate);
}

if (request.internalPublic != null) {
_request.append("internalPublic", request.internalPublic);
}

if (request.sourceSystemUpdated != null) {
_request.append("sourceSystemUpdated", request.sourceSystemUpdated);
}

if (request.awaitVectorStoreSync != null) {
_request.append("awaitVectorStoreSync", request.awaitVectorStoreSync);
}

const _maybeEncodedRequest = await _request.getRequest();
const _headers: core.Fetcher.Args["headers"] = mergeHeaders(
this._options?.headers,
mergeOnlyDefinedHeaders({
Authorization: await this._getAuthorizationHeader(),
..._maybeEncodedRequest.headers,
}),
requestOptions?.headers,
);
const _response = await (this._options.fetcher ?? core.fetcher)({
url: core.url.join(
(await core.Supplier.get(this._options.baseUrl)) ??
(await core.Supplier.get(this._options.environment)) ??
environments.CredalEnvironment.Production,
"/v0/catalog/uploadFile",
),
method: "POST",
headers: _headers,
queryParameters: requestOptions?.queryParams,
requestType: "file",
duplex: _maybeEncodedRequest.duplex,
body: _maybeEncodedRequest.body,
timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000,
maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries,
abortSignal: requestOptions?.abortSignal,
});
if (_response.ok) {
return { data: _response.body as Credal.UploadDocumentResponse, rawResponse: _response.rawResponse };
}

if (_response.error.reason === "status-code") {
throw new errors.CredalError({
statusCode: _response.error.statusCode,
body: _response.error.body,
rawResponse: _response.rawResponse,
});
}

switch (_response.error.reason) {
case "non-json":
throw new errors.CredalError({
statusCode: _response.error.statusCode,
body: _response.error.rawBody,
rawResponse: _response.rawResponse,
});
case "timeout":
throw new errors.CredalTimeoutError("Timeout exceeded when calling POST /v0/catalog/uploadFile.");
case "unknown":
throw new errors.CredalError({
message: _response.error.errorMessage,
rawResponse: _response.rawResponse,
});
}
}

/**
* Sync a document from a source URL. Does not support recursive web search. Reach out to a Credal representative for access.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// This file was auto-generated by Fern from our API Definition.

import type * as core from "../../../../../core/index.js";

export interface UploadFileRequest {
/** The file to upload. Supported formats: PDF (.pdf), Word (.docx), Excel (.xlsx, .xls), PowerPoint (.pptx, .ppt), CSV (.csv), and text files. */
file: core.file.Uploadable;
/** The name of the document you want to upload. If not provided, the original filename will be used. */
documentName?: string;
/** [Legacy] The user on behalf of whom the document should be uploaded. In most cases, this can simply be the email of the developer making the API call. This field will be removed in the future in favor of purely specifying permissions via allowedUsersEmailAddresses. */
uploadAsUserEmail: string;
/** The external ID of the document. This is typically the ID as it exists in its original external system. Uploads to the same external ID will update the document in Credal. */
documentExternalId: string;
/** Users allowed to access the document. Can be provided as a JSON array string (e.g., ["user1@example.com","user2@example.com"]) or comma-separated list (e.g., "user1@example.com,user2@example.com"). Unlike Credal's out of the box connectors which reconcile various permissions models from 3rd party software, for custom uploads the caller is responsible for specifying who can access the document and currently flattening groups if applicable. Documents can also be marked as internal public. */
allowedUsersEmailAddresses?: string;
/** The external URL of the document you want to upload. If provided Credal will link to this URL. */
documentExternalUrl?: string;
/** Optional JSON string representing any custom metadata for this document (e.g., '{"key1":"value1","key2":"value2"}'). */
customMetadata?: string;
/** If specified, the document will also be added to the provided document collection. This operation is eventually consistent, meaning the document does not immediately start appearing in searches of that collection due to an asynchronous embedding process. To achieve strong consistency use the `awaitVectorStoreSync` parameter. */
collectionId?: string;
/** If set to "true", document contents will be re-uploaded and re-embedded even if the document already exists in Credal. */
forceUpdate?: string;
/** If set to "true", document will be accessible to everyone within the organization of the uploader. */
internalPublic?: string;
/** ISO 8601 date string indicating when the document was last updated in the source system (e.g., "2025-11-03T21:15:00Z"). */
sourceSystemUpdated?: string;
/** Document uploads are eventually consistent by default. If set to "true" the API will wait for the vector store to be updated before returning. This is useful if you want to ensure that the document is immediately searchable after this call returns. */
awaitVectorStoreSync?: string;
}
1 change: 1 addition & 0 deletions src/api/resources/documentCatalog/client/requests/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export type { UploadDocumentContentsRequest } from "./UploadDocumentContentsRequest.js";
export type { UploadFileRequest } from "./UploadFileRequest.js";
1 change: 1 addition & 0 deletions src/core/exports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./file/exports.js";
1 change: 1 addition & 0 deletions src/core/file/exports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { Uploadable } from "./types.js";
217 changes: 217 additions & 0 deletions src/core/file/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import type { Uploadable } from "./types.js";

export async function toBinaryUploadRequest(
file: Uploadable,
): Promise<{ body: Uploadable.FileLike; headers?: Record<string, string> }> {
const { data, filename, contentLength, contentType } = await getFileWithMetadata(file);
const request = {
body: data,
headers: {} as Record<string, string>,
};
if (filename) {
request.headers["Content-Disposition"] = `attachment; filename="${filename}"`;
}
if (contentType) {
request.headers["Content-Type"] = contentType;
}
if (contentLength != null) {
request.headers["Content-Length"] = contentLength.toString();
}
return request;
}

export async function toMultipartDataPart(
file: Uploadable,
): Promise<{ data: Uploadable.FileLike; filename?: string; contentType?: string }> {
const { data, filename, contentType } = await getFileWithMetadata(file, {
noSniffFileSize: true,
});
return {
data,
filename,
contentType,
};
}

async function getFileWithMetadata(
file: Uploadable,
{ noSniffFileSize }: { noSniffFileSize?: boolean } = {},
): Promise<Uploadable.WithMetadata> {
if (isFileLike(file)) {
return getFileWithMetadata(
{
data: file,
},
{ noSniffFileSize },
);
}

if ("path" in file) {
const fs = await import("fs");
if (!fs || !fs.createReadStream) {
throw new Error("File path uploads are not supported in this environment.");
}
const data = fs.createReadStream(file.path);
const contentLength =
file.contentLength ?? (noSniffFileSize === true ? undefined : await tryGetFileSizeFromPath(file.path));
const filename = file.filename ?? getNameFromPath(file.path);
return {
data,
filename,
contentType: file.contentType,
contentLength,
};
}
if ("data" in file) {
const data = file.data;
const contentLength =
file.contentLength ??
(await tryGetContentLengthFromFileLike(data, {
noSniffFileSize,
}));
const filename = file.filename ?? tryGetNameFromFileLike(data);
return {
data,
filename,
contentType: file.contentType ?? tryGetContentTypeFromFileLike(data),
contentLength,
};
}

throw new Error(`Invalid FileUpload of type ${typeof file}: ${JSON.stringify(file)}`);
}

function isFileLike(value: unknown): value is Uploadable.FileLike {
return (
isBuffer(value) ||
isArrayBufferView(value) ||
isArrayBuffer(value) ||
isUint8Array(value) ||
isBlob(value) ||
isFile(value) ||
isStreamLike(value) ||
isReadableStream(value)
);
}

async function tryGetFileSizeFromPath(path: string): Promise<number | undefined> {
try {
const fs = await import("fs");
if (!fs || !fs.promises || !fs.promises.stat) {
return undefined;
}
const fileStat = await fs.promises.stat(path);
return fileStat.size;
} catch (_fallbackError) {
return undefined;
}
}

function tryGetNameFromFileLike(data: Uploadable.FileLike): string | undefined {
if (isNamedValue(data)) {
return data.name;
}
if (isPathedValue(data)) {
return getNameFromPath(data.path.toString());
}
return undefined;
}

async function tryGetContentLengthFromFileLike(
data: Uploadable.FileLike,
{ noSniffFileSize }: { noSniffFileSize?: boolean } = {},
): Promise<number | undefined> {
if (isBuffer(data)) {
return data.length;
}
if (isArrayBufferView(data)) {
return data.byteLength;
}
if (isArrayBuffer(data)) {
return data.byteLength;
}
if (isBlob(data)) {
return data.size;
}
if (isFile(data)) {
return data.size;
}
if (noSniffFileSize === true) {
return undefined;
}
if (isPathedValue(data)) {
return await tryGetFileSizeFromPath(data.path.toString());
}
return undefined;
}

function tryGetContentTypeFromFileLike(data: Uploadable.FileLike): string | undefined {
if (isBlob(data)) {
return data.type;
}
if (isFile(data)) {
return data.type;
}

return undefined;
}

function getNameFromPath(path: string): string | undefined {
const lastForwardSlash = path.lastIndexOf("/");
const lastBackSlash = path.lastIndexOf("\\");
const lastSlashIndex = Math.max(lastForwardSlash, lastBackSlash);
return lastSlashIndex >= 0 ? path.substring(lastSlashIndex + 1) : path;
}

type NamedValue = {
name: string;
} & unknown;

type PathedValue = {
path: string | { toString(): string };
} & unknown;

type StreamLike = {
read?: () => unknown;
pipe?: (dest: unknown) => unknown;
} & unknown;

function isNamedValue(value: unknown): value is NamedValue {
return typeof value === "object" && value != null && "name" in value;
}

function isPathedValue(value: unknown): value is PathedValue {
return typeof value === "object" && value != null && "path" in value;
}

function isStreamLike(value: unknown): value is StreamLike {
return typeof value === "object" && value != null && ("read" in value || "pipe" in value);
}

function isReadableStream(value: unknown): value is ReadableStream {
return typeof value === "object" && value != null && "getReader" in value;
}

function isBuffer(value: unknown): value is Buffer {
return typeof Buffer !== "undefined" && Buffer.isBuffer && Buffer.isBuffer(value);
}

function isArrayBufferView(value: unknown): value is ArrayBufferView {
return typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(value);
}

function isArrayBuffer(value: unknown): value is ArrayBuffer {
return typeof ArrayBuffer !== "undefined" && value instanceof ArrayBuffer;
}

function isUint8Array(value: unknown): value is Uint8Array {
return typeof Uint8Array !== "undefined" && value instanceof Uint8Array;
}

function isBlob(value: unknown): value is Blob {
return typeof Blob !== "undefined" && value instanceof Blob;
}

function isFile(value: unknown): value is File {
return typeof File !== "undefined" && value instanceof File;
}
2 changes: 2 additions & 0 deletions src/core/file/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./file.js";
export * from "./types.js";
Loading