@renderinc/sdk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +15 -15
- package/biome.json +84 -0
- package/dist/experimental/blob/api.d.ts +11 -0
- package/dist/experimental/blob/api.d.ts.map +1 -0
- package/dist/experimental/blob/api.js +44 -0
- package/dist/experimental/blob/client.d.ts +21 -0
- package/dist/experimental/blob/client.d.ts.map +1 -0
- package/dist/experimental/blob/client.js +127 -0
- package/dist/experimental/blob/index.d.ts +5 -0
- package/dist/experimental/blob/index.d.ts.map +1 -0
- package/dist/experimental/blob/index.js +8 -0
- package/dist/experimental/blob/types.d.ts +49 -0
- package/dist/experimental/blob/types.d.ts.map +1 -0
- package/dist/experimental/blob/types.js +2 -0
- package/dist/experimental/experimental.d.ts +8 -0
- package/dist/experimental/experimental.d.ts.map +1 -0
- package/dist/experimental/experimental.js +10 -0
- package/dist/experimental/index.d.ts +3 -0
- package/dist/experimental/index.d.ts.map +1 -0
- package/dist/experimental/index.js +9 -0
- package/dist/generated/schema.d.ts +9782 -0
- package/dist/generated/schema.d.ts.map +1 -0
- package/dist/generated/schema.js +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/render.d.ts +2 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +4 -2
- package/dist/utils/create-api-client.d.ts +1 -1
- package/dist/utils/create-api-client.d.ts.map +1 -1
- package/dist/utils/create-api-client.js +2 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +23 -0
- package/dist/workflows/client/client.d.ts +1 -1
- package/dist/workflows/client/client.d.ts.map +1 -1
- package/dist/workflows/client/sse.d.ts +2 -2
- package/dist/workflows/client/sse.d.ts.map +1 -1
- package/dist/workflows/client/sse.js +2 -0
- package/dist/workflows/client/types.d.ts +1 -1
- package/dist/workflows/client/types.d.ts.map +1 -1
- package/dist/workflows/executor.d.ts +2 -2
- package/dist/workflows/executor.d.ts.map +1 -1
- package/dist/workflows/registry.d.ts +1 -1
- package/dist/workflows/registry.d.ts.map +1 -1
- package/dist/workflows/registry.js +13 -6
- package/dist/workflows/runner.d.ts.map +1 -1
- package/dist/workflows/runner.js +2 -0
- package/dist/workflows/schema.d.ts +9 -0
- package/dist/workflows/schema.d.ts.map +1 -1
- package/dist/workflows/task.d.ts +1 -0
- package/dist/workflows/task.d.ts.map +1 -1
- package/dist/workflows/task.js +34 -0
- package/dist/workflows/types.d.ts +3 -1
- package/dist/workflows/types.d.ts.map +1 -1
- package/dist/workflows/uds.d.ts +1 -1
- package/dist/workflows/uds.d.ts.map +1 -1
- package/dist/workflows/uds.js +9 -39
- package/examples/client/main.ts +42 -0
- package/examples/client/package-lock.json +601 -0
- package/examples/client/package.json +16 -0
- package/examples/client/tsconfig.json +17 -0
- package/examples/task/main.ts +90 -0
- package/examples/task/package-lock.json +585 -0
- package/examples/task/package.json +16 -0
- package/examples/task/tsconfig.json +17 -0
- package/package.json +13 -22
- package/src/errors.ts +73 -0
- package/src/experimental/blob/api.ts +91 -0
- package/src/experimental/blob/client.ts +317 -0
- package/src/experimental/blob/index.ts +22 -0
- package/src/experimental/blob/types.ts +131 -0
- package/src/experimental/experimental.ts +33 -0
- package/src/experimental/index.ts +24 -0
- package/src/generated/schema.ts +12729 -0
- package/src/index.ts +7 -0
- package/src/render.ts +35 -0
- package/src/utils/create-api-client.ts +13 -0
- package/src/utils/get-base-url.ts +16 -0
- package/src/version.ts +37 -0
- package/src/workflows/client/client.ts +142 -0
- package/src/workflows/client/create-client.ts +17 -0
- package/src/workflows/client/index.ts +3 -0
- package/src/workflows/client/sse.ts +95 -0
- package/src/workflows/client/types.ts +56 -0
- package/src/workflows/executor.ts +124 -0
- package/src/workflows/index.ts +7 -0
- package/src/workflows/registry.test.ts +76 -0
- package/src/workflows/registry.ts +88 -0
- package/src/workflows/runner.ts +38 -0
- package/src/workflows/schema.ts +348 -0
- package/src/workflows/task.ts +117 -0
- package/src/workflows/types.ts +89 -0
- package/src/workflows/uds.ts +179 -0
- package/test-types.ts +14 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +23 -0
- package/vite.config.ts +7 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": ".",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"moduleResolution": "NodeNext",
|
|
13
|
+
"resolveJsonModule": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["**/*.ts"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@renderinc/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Render SDK for TypeScript",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"build": "tsc",
|
|
8
|
+
"build": "tsc -p tsconfig.build.json",
|
|
9
9
|
"test": "vitest",
|
|
10
|
-
"lint": "biome lint
|
|
11
|
-
"lint:fix": "biome lint --write
|
|
12
|
-
"format": "biome format --write
|
|
13
|
-
"format:check": "biome format
|
|
14
|
-
"check": "biome check --write
|
|
10
|
+
"lint": "biome lint",
|
|
11
|
+
"lint:fix": "biome lint --write",
|
|
12
|
+
"format": "biome format --write",
|
|
13
|
+
"format:check": "biome format",
|
|
14
|
+
"check": "biome check --write",
|
|
15
15
|
"prepublishOnly": "npm run build"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
@@ -22,15 +22,6 @@
|
|
|
22
22
|
],
|
|
23
23
|
"author": "Render",
|
|
24
24
|
"license": "MIT",
|
|
25
|
-
"repository": {
|
|
26
|
-
"type": "git",
|
|
27
|
-
"url": "https://github.com/render-oss/sdk.git",
|
|
28
|
-
"directory": "typescript"
|
|
29
|
-
},
|
|
30
|
-
"homepage": "https://github.com/render-oss/sdk/tree/main/typescript#readme",
|
|
31
|
-
"bugs": {
|
|
32
|
-
"url": "https://github.com/render-oss/sdk/issues"
|
|
33
|
-
},
|
|
34
25
|
"devDependencies": {
|
|
35
26
|
"@biomejs/biome": "2.3.8",
|
|
36
27
|
"@types/node": "^20.0.0",
|
|
@@ -55,11 +46,11 @@
|
|
|
55
46
|
"import": "./dist/workflows/index.js",
|
|
56
47
|
"require": "./dist/workflows/index.js",
|
|
57
48
|
"types": "./dist/workflows/index.d.ts"
|
|
49
|
+
},
|
|
50
|
+
"./experimental": {
|
|
51
|
+
"import": "./dist/experimental/index.js",
|
|
52
|
+
"require": "./dist/experimental/index.js",
|
|
53
|
+
"types": "./dist/experimental/index.d.ts"
|
|
58
54
|
}
|
|
59
|
-
}
|
|
60
|
-
"files": [
|
|
61
|
-
"dist",
|
|
62
|
-
"README.md",
|
|
63
|
-
"LICENSE"
|
|
64
|
-
]
|
|
55
|
+
}
|
|
65
56
|
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for all Render SDK errors
|
|
3
|
+
*/
|
|
4
|
+
export class RenderError extends Error {
|
|
5
|
+
constructor(message: string) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "RenderError";
|
|
8
|
+
Object.setPrototypeOf(this, RenderError.prototype);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Error for task execution failures
|
|
14
|
+
*/
|
|
15
|
+
export class TaskRunError extends RenderError {
|
|
16
|
+
constructor(
|
|
17
|
+
message: string,
|
|
18
|
+
public taskRunId?: string,
|
|
19
|
+
public taskError?: string,
|
|
20
|
+
) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "TaskRunError";
|
|
23
|
+
Object.setPrototypeOf(this, TaskRunError.prototype);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Error for HTTP client errors (4xx)
|
|
29
|
+
*/
|
|
30
|
+
export class ClientError extends RenderError {
|
|
31
|
+
constructor(
|
|
32
|
+
message: string,
|
|
33
|
+
public statusCode: number,
|
|
34
|
+
public response?: any,
|
|
35
|
+
) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "ClientError";
|
|
38
|
+
Object.setPrototypeOf(this, ClientError.prototype);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Error for HTTP server errors (5xx)
|
|
44
|
+
*/
|
|
45
|
+
export class ServerError extends RenderError {
|
|
46
|
+
constructor(
|
|
47
|
+
message: string,
|
|
48
|
+
public statusCode: number,
|
|
49
|
+
public response?: any,
|
|
50
|
+
) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "ServerError";
|
|
53
|
+
Object.setPrototypeOf(this, ServerError.prototype);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Error for request timeouts
|
|
59
|
+
*/
|
|
60
|
+
export class TimeoutError extends RenderError {
|
|
61
|
+
constructor(message: string) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = "TimeoutError";
|
|
64
|
+
Object.setPrototypeOf(this, TimeoutError.prototype);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class AbortError extends Error {
|
|
69
|
+
constructor() {
|
|
70
|
+
super("The operation was aborted.");
|
|
71
|
+
this.name = "AbortError";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Client } from "openapi-fetch";
|
|
2
|
+
import { RenderError } from "../../errors.js";
|
|
3
|
+
import type { paths } from "../../generated/schema.js";
|
|
4
|
+
import type { PresignedDownloadUrl, PresignedUploadUrl, Region } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Layer 2: Typed Blob API Client
|
|
8
|
+
*
|
|
9
|
+
* Provides idiomatic TypeScript wrapper around the raw OpenAPI client.
|
|
10
|
+
* Handles presigned URL flow but still exposes the two-step nature
|
|
11
|
+
* (get URL, then upload/download). Useful for advanced use cases
|
|
12
|
+
* requiring fine-grained control.
|
|
13
|
+
*/
|
|
14
|
+
export class BlobApi {
|
|
15
|
+
constructor(private readonly apiClient: Client<paths>) {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get a presigned URL for uploading a blob
|
|
19
|
+
*
|
|
20
|
+
* @param ownerId - Owner ID (workspace team ID)
|
|
21
|
+
* @param region - Storage region
|
|
22
|
+
* @param key - Object key (path)
|
|
23
|
+
* @param sizeBytes - Size of the blob in bytes
|
|
24
|
+
* @returns Presigned upload URL with expiration and size limit
|
|
25
|
+
*/
|
|
26
|
+
async getUploadUrl(
|
|
27
|
+
ownerId: string,
|
|
28
|
+
region: Region | string,
|
|
29
|
+
key: string,
|
|
30
|
+
sizeBytes: number,
|
|
31
|
+
): Promise<PresignedUploadUrl> {
|
|
32
|
+
const { data, error } = await this.apiClient.PUT("/blobs/{ownerId}/{region}/{key}", {
|
|
33
|
+
params: { path: { ownerId, region: region as Region, key } },
|
|
34
|
+
body: { sizeBytes },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (error) {
|
|
38
|
+
throw new RenderError(`Failed to get upload URL: ${error.message || "Unknown error"}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
url: data.url,
|
|
43
|
+
expiresAt: new Date(data.expiresAt),
|
|
44
|
+
maxSizeBytes: data.maxSizeBytes,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get a presigned URL for downloading a blob
|
|
50
|
+
*
|
|
51
|
+
* @param ownerId - Owner ID (workspace team ID)
|
|
52
|
+
* @param region - Storage region
|
|
53
|
+
* @param key - Object key (path)
|
|
54
|
+
* @returns Presigned download URL with expiration
|
|
55
|
+
*/
|
|
56
|
+
async getDownloadUrl(
|
|
57
|
+
ownerId: string,
|
|
58
|
+
region: Region | string,
|
|
59
|
+
key: string,
|
|
60
|
+
): Promise<PresignedDownloadUrl> {
|
|
61
|
+
const { data, error } = await this.apiClient.GET("/blobs/{ownerId}/{region}/{key}", {
|
|
62
|
+
params: { path: { ownerId, region: region as Region, key } },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (error) {
|
|
66
|
+
throw new RenderError(`Failed to get download URL: ${error.message || "Unknown error"}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
url: data.url,
|
|
71
|
+
expiresAt: new Date(data.expiresAt),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Delete a blob
|
|
77
|
+
*
|
|
78
|
+
* @param ownerId - Owner ID (workspace team ID)
|
|
79
|
+
* @param region - Storage region
|
|
80
|
+
* @param key - Object key (path)
|
|
81
|
+
*/
|
|
82
|
+
async delete(ownerId: string, region: Region | string, key: string): Promise<void> {
|
|
83
|
+
const { error } = await this.apiClient.DELETE("/blobs/{ownerId}/{region}/{key}", {
|
|
84
|
+
params: { path: { ownerId, region: region as Region, key } },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (error) {
|
|
88
|
+
throw new RenderError(`Failed to delete blob: ${error.message || "Unknown error"}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import type { Client } from "openapi-fetch";
|
|
2
|
+
import { RenderError } from "../../errors.js";
|
|
3
|
+
import type { paths } from "../../generated/schema.js";
|
|
4
|
+
import type {
|
|
5
|
+
BlobData,
|
|
6
|
+
BlobScope,
|
|
7
|
+
DeleteBlobInput,
|
|
8
|
+
GetBlobInput,
|
|
9
|
+
PutBlobInput,
|
|
10
|
+
PutBlobResult,
|
|
11
|
+
Region,
|
|
12
|
+
ScopedDeleteBlobInput,
|
|
13
|
+
ScopedGetBlobInput,
|
|
14
|
+
ScopedPutBlobInput,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Layer 3: High-Level Blob Client
|
|
19
|
+
*
|
|
20
|
+
* User-facing API that abstracts presigned URLs completely.
|
|
21
|
+
* Provides simple put/get/delete operations that handle the
|
|
22
|
+
* two-step presigned URL flow internally.
|
|
23
|
+
*/
|
|
24
|
+
export class BlobClient {
|
|
25
|
+
constructor(private readonly apiClient: Client<paths>) {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Upload a blob to storage
|
|
29
|
+
*
|
|
30
|
+
* @param input - Upload parameters including blob identifier and data
|
|
31
|
+
* @returns Result with optional ETag
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* // Upload a Buffer
|
|
36
|
+
* const data = Buffer.from("binary content");
|
|
37
|
+
* await blobClient.put({
|
|
38
|
+
* ownerId: "tea-xxxxx",
|
|
39
|
+
* region: "oregon",
|
|
40
|
+
* key: "path/to/file.png",
|
|
41
|
+
* data,
|
|
42
|
+
* contentType: "image/png"
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* // Upload from stream
|
|
46
|
+
* const stream = createReadStream("/path/to/file.zip");
|
|
47
|
+
* const stats = statSync("/path/to/file.zip");
|
|
48
|
+
* await blobClient.put({
|
|
49
|
+
* ownerId: "tea-xxxxx",
|
|
50
|
+
* region: "oregon",
|
|
51
|
+
* key: "file.zip",
|
|
52
|
+
* data: stream,
|
|
53
|
+
* size: stats.size
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
async put(input: PutBlobInput): Promise<PutBlobResult> {
|
|
58
|
+
// Resolve and validate size
|
|
59
|
+
const size = this.resolveSize(input);
|
|
60
|
+
|
|
61
|
+
// Step 1: Get presigned upload URL from Render API
|
|
62
|
+
const { data, error } = await this.apiClient.PUT("/blobs/{ownerId}/{region}/{key}", {
|
|
63
|
+
params: {
|
|
64
|
+
path: {
|
|
65
|
+
ownerId: input.ownerId,
|
|
66
|
+
region: input.region as Region,
|
|
67
|
+
key: input.key,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
body: { sizeBytes: size },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (error) {
|
|
74
|
+
throw new RenderError(`Failed to get upload URL: ${error.message || "Unknown error"}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Step 2: Upload to storage via presigned URL
|
|
78
|
+
const headers: Record<string, string> = {
|
|
79
|
+
"Content-Length": size.toString(),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (input.contentType) {
|
|
83
|
+
headers["Content-Type"] = input.contentType;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const response = await fetch(data.url, {
|
|
87
|
+
method: "PUT",
|
|
88
|
+
headers,
|
|
89
|
+
body: input.data,
|
|
90
|
+
duplex: "half",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw new RenderError(`Upload failed: ${response.status} ${response.statusText}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
etag: response.headers.get("ETag") ?? undefined,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Download a blob from storage
|
|
104
|
+
*
|
|
105
|
+
* @param input - Download parameters including blob identifier
|
|
106
|
+
* @returns Blob data with content
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* const blob = await blobClient.get({
|
|
111
|
+
* ownerId: "tea-xxxxx",
|
|
112
|
+
* region: "oregon",
|
|
113
|
+
* key: "path/to/file.png"
|
|
114
|
+
* });
|
|
115
|
+
*
|
|
116
|
+
* console.log(blob.size); // Size in bytes
|
|
117
|
+
* console.log(blob.contentType); // MIME type if available
|
|
118
|
+
* // blob.data is a Buffer
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
async get(input: GetBlobInput): Promise<BlobData> {
|
|
122
|
+
// Step 1: Get presigned download URL from Render API
|
|
123
|
+
const { data, error } = await this.apiClient.GET("/blobs/{ownerId}/{region}/{key}", {
|
|
124
|
+
params: {
|
|
125
|
+
path: {
|
|
126
|
+
ownerId: input.ownerId,
|
|
127
|
+
region: input.region as Region,
|
|
128
|
+
key: input.key,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (error) {
|
|
134
|
+
throw new RenderError(`Failed to get download URL: ${error.message || "Unknown error"}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Step 2: Download from storage via presigned URL
|
|
138
|
+
const response = await fetch(data.url);
|
|
139
|
+
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
throw new RenderError(`Download failed: ${response.status} ${response.statusText}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
145
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
data: buffer,
|
|
149
|
+
size: buffer.byteLength,
|
|
150
|
+
contentType: response.headers.get("Content-Type") ?? undefined,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Delete a blob from storage
|
|
156
|
+
*
|
|
157
|
+
* @param input - Delete parameters including blob identifier
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* await blobClient.delete({
|
|
162
|
+
* ownerId: "tea-xxxxx",
|
|
163
|
+
* region: "oregon",
|
|
164
|
+
* key: "path/to/file.png"
|
|
165
|
+
* });
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
async delete(input: DeleteBlobInput): Promise<void> {
|
|
169
|
+
// DELETE goes directly to Render API (no presigned URL)
|
|
170
|
+
const { error } = await this.apiClient.DELETE("/blobs/{ownerId}/{region}/{key}", {
|
|
171
|
+
params: {
|
|
172
|
+
path: {
|
|
173
|
+
ownerId: input.ownerId,
|
|
174
|
+
region: input.region as Region,
|
|
175
|
+
key: input.key,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (error) {
|
|
181
|
+
throw new RenderError(`Failed to delete blob: ${error.message || "Unknown error"}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Create a scoped blob client for a specific owner and region
|
|
187
|
+
*
|
|
188
|
+
* @param scope - Owner ID and region to scope operations to
|
|
189
|
+
* @returns Scoped blob client that doesn't require ownerId/region on each call
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```typescript
|
|
193
|
+
* const scoped = blobClient.scoped({
|
|
194
|
+
* ownerId: "tea-xxxxx",
|
|
195
|
+
* region: "oregon"
|
|
196
|
+
* });
|
|
197
|
+
*
|
|
198
|
+
* // Subsequent calls only need the key
|
|
199
|
+
* await scoped.put({ key: "file.png", data: buffer });
|
|
200
|
+
* await scoped.get({ key: "file.png" });
|
|
201
|
+
* await scoped.delete({ key: "file.png" });
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
scoped(scope: BlobScope): ScopedBlobClient {
|
|
205
|
+
return new ScopedBlobClient(this.apiClient, scope);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Resolve and validate the size for a put operation
|
|
210
|
+
*
|
|
211
|
+
* - For Buffer/Uint8Array: auto-calculate size, validate if provided
|
|
212
|
+
* - For streams/strings: require explicit size
|
|
213
|
+
*/
|
|
214
|
+
private resolveSize(input: PutBlobInput): number {
|
|
215
|
+
if (Buffer.isBuffer(input.data) || input.data instanceof Uint8Array) {
|
|
216
|
+
const actualSize = input.data.byteLength;
|
|
217
|
+
|
|
218
|
+
if (input.size !== undefined && input.size !== actualSize) {
|
|
219
|
+
throw new RenderError(
|
|
220
|
+
`Size mismatch: provided size ${input.size} does not match actual size ${actualSize}`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return actualSize;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// For Readable streams or strings, size must be provided
|
|
228
|
+
if (input.size === undefined) {
|
|
229
|
+
throw new RenderError(
|
|
230
|
+
"Size is required for stream and string inputs. Provide the size parameter.",
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (input.size <= 0) {
|
|
235
|
+
throw new RenderError("Size must be a positive integer");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return input.size;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Scoped Blob Client
|
|
244
|
+
*
|
|
245
|
+
* Pre-configured client for a specific owner and region.
|
|
246
|
+
* Eliminates the need to specify ownerId and region on every operation.
|
|
247
|
+
*/
|
|
248
|
+
export class ScopedBlobClient {
|
|
249
|
+
private readonly blobClient: BlobClient;
|
|
250
|
+
|
|
251
|
+
constructor(
|
|
252
|
+
apiClient: Client<paths>,
|
|
253
|
+
private readonly scope: BlobScope,
|
|
254
|
+
) {
|
|
255
|
+
this.blobClient = new BlobClient(apiClient);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Upload a blob to storage using scoped owner and region
|
|
260
|
+
*
|
|
261
|
+
* @param input - Upload parameters (key and data only)
|
|
262
|
+
* @returns Result with optional ETag
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```typescript
|
|
266
|
+
* const scoped = blobClient.scoped({ ownerId: "tea-xxxxx", region: "oregon" });
|
|
267
|
+
* await scoped.put({
|
|
268
|
+
* key: "file.png",
|
|
269
|
+
* data: Buffer.from("content"),
|
|
270
|
+
* contentType: "image/png"
|
|
271
|
+
* });
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
async put(input: ScopedPutBlobInput): Promise<PutBlobResult> {
|
|
275
|
+
return this.blobClient.put({
|
|
276
|
+
...this.scope,
|
|
277
|
+
...input,
|
|
278
|
+
} as PutBlobInput);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Download a blob from storage using scoped owner and region
|
|
283
|
+
*
|
|
284
|
+
* @param input - Download parameters (key only)
|
|
285
|
+
* @returns Blob data with content
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```typescript
|
|
289
|
+
* const scoped = blobClient.scoped({ ownerId: "tea-xxxxx", region: "oregon" });
|
|
290
|
+
* const blob = await scoped.get({ key: "file.png" });
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
async get(input: ScopedGetBlobInput): Promise<BlobData> {
|
|
294
|
+
return this.blobClient.get({
|
|
295
|
+
...this.scope,
|
|
296
|
+
...input,
|
|
297
|
+
} as GetBlobInput);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Delete a blob from storage using scoped owner and region
|
|
302
|
+
*
|
|
303
|
+
* @param input - Delete parameters (key only)
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```typescript
|
|
307
|
+
* const scoped = blobClient.scoped({ ownerId: "tea-xxxxx", region: "oregon" });
|
|
308
|
+
* await scoped.delete({ key: "file.png" });
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
async delete(input: ScopedDeleteBlobInput): Promise<void> {
|
|
312
|
+
return this.blobClient.delete({
|
|
313
|
+
...this.scope,
|
|
314
|
+
...input,
|
|
315
|
+
} as DeleteBlobInput);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Blob storage client exports
|
|
2
|
+
export { BlobApi } from "./api.js";
|
|
3
|
+
export { BlobClient, ScopedBlobClient } from "./client.js";
|
|
4
|
+
// Type exports
|
|
5
|
+
export type {
|
|
6
|
+
BlobData,
|
|
7
|
+
BlobIdentifier,
|
|
8
|
+
BlobScope,
|
|
9
|
+
DeleteBlobInput,
|
|
10
|
+
GetBlobInput,
|
|
11
|
+
PresignedDownloadUrl,
|
|
12
|
+
PresignedUploadUrl,
|
|
13
|
+
PutBlobInput,
|
|
14
|
+
PutBlobInputBuffer,
|
|
15
|
+
PutBlobInputStream,
|
|
16
|
+
PutBlobResult,
|
|
17
|
+
ScopedDeleteBlobInput,
|
|
18
|
+
ScopedGetBlobInput,
|
|
19
|
+
ScopedPutBlobInput,
|
|
20
|
+
} from "./types.js";
|
|
21
|
+
// Re-export the Region enum (both type and value)
|
|
22
|
+
export { Region } from "./types.js";
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Supported regions for blob storage
|
|
5
|
+
*/
|
|
6
|
+
export type Region = "frankfurt" | "oregon" | "ohio" | "singapore" | "virginia";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base identifier for a blob object
|
|
10
|
+
*/
|
|
11
|
+
export interface BlobIdentifier {
|
|
12
|
+
/** Owner ID (workspace team ID) in format tea-xxxxx */
|
|
13
|
+
ownerId: `tea-${string}`;
|
|
14
|
+
/** Region where the blob is stored */
|
|
15
|
+
region: Region | string;
|
|
16
|
+
/** Object key (path) for the blob */
|
|
17
|
+
key: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Base options for putting a blob
|
|
22
|
+
*/
|
|
23
|
+
interface PutBlobInputBase extends BlobIdentifier {
|
|
24
|
+
/** MIME type of the content (optional, will be auto-detected if not provided) */
|
|
25
|
+
contentType?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Put blob input for Buffer, Uint8Array, or string data
|
|
30
|
+
* Size is optional and will be auto-calculated
|
|
31
|
+
*/
|
|
32
|
+
export interface PutBlobInputBuffer extends PutBlobInputBase {
|
|
33
|
+
/** Binary data as Buffer, Uint8Array, or string */
|
|
34
|
+
data: Buffer | Uint8Array | string;
|
|
35
|
+
/** Size in bytes (optional, auto-calculated for Buffer/Uint8Array) */
|
|
36
|
+
size?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Put blob input for readable streams
|
|
41
|
+
* Size is required for streams
|
|
42
|
+
*/
|
|
43
|
+
export interface PutBlobInputStream extends PutBlobInputBase {
|
|
44
|
+
/** Readable stream */
|
|
45
|
+
data: Readable;
|
|
46
|
+
/** Size in bytes (required for streams) */
|
|
47
|
+
size: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Input for uploading a blob
|
|
52
|
+
* Discriminated union: size is optional for Buffer/Uint8Array, required for streams
|
|
53
|
+
*/
|
|
54
|
+
export type PutBlobInput = PutBlobInputBuffer | PutBlobInputStream;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Input for downloading a blob
|
|
58
|
+
*/
|
|
59
|
+
export interface GetBlobInput extends BlobIdentifier {}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Input for deleting a blob
|
|
63
|
+
*/
|
|
64
|
+
export interface DeleteBlobInput extends BlobIdentifier {}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Presigned URL for uploading
|
|
68
|
+
*/
|
|
69
|
+
export interface PresignedUploadUrl {
|
|
70
|
+
/** Presigned upload URL */
|
|
71
|
+
url: string;
|
|
72
|
+
/** Expiration timestamp */
|
|
73
|
+
expiresAt: Date;
|
|
74
|
+
/** Maximum size allowed for upload */
|
|
75
|
+
maxSizeBytes: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Presigned URL for downloading
|
|
80
|
+
*/
|
|
81
|
+
export interface PresignedDownloadUrl {
|
|
82
|
+
/** Presigned download URL */
|
|
83
|
+
url: string;
|
|
84
|
+
/** Expiration timestamp */
|
|
85
|
+
expiresAt: Date;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Downloaded blob data
|
|
90
|
+
*/
|
|
91
|
+
export interface BlobData {
|
|
92
|
+
/** Binary content */
|
|
93
|
+
data: Buffer;
|
|
94
|
+
/** MIME type if available */
|
|
95
|
+
contentType?: string;
|
|
96
|
+
/** Size in bytes */
|
|
97
|
+
size: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Result from uploading a blob
|
|
102
|
+
*/
|
|
103
|
+
export interface PutBlobResult {
|
|
104
|
+
/** ETag from storage provider */
|
|
105
|
+
etag?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Scope configuration for scoped blob client
|
|
110
|
+
*/
|
|
111
|
+
export interface BlobScope {
|
|
112
|
+
/** Owner ID (workspace team ID) in format tea-xxxxx */
|
|
113
|
+
ownerId: `tea-${string}`;
|
|
114
|
+
/** Region where the blob is stored */
|
|
115
|
+
region: Region | string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Scoped input for uploading a blob (without ownerId/region)
|
|
120
|
+
*/
|
|
121
|
+
export type ScopedPutBlobInput = Omit<PutBlobInput, keyof BlobScope>;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Scoped input for downloading a blob (without ownerId/region)
|
|
125
|
+
*/
|
|
126
|
+
export type ScopedGetBlobInput = Omit<GetBlobInput, keyof BlobScope>;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Scoped input for deleting a blob (without ownerId/region)
|
|
130
|
+
*/
|
|
131
|
+
export type ScopedDeleteBlobInput = Omit<DeleteBlobInput, keyof BlobScope>;
|