@renderinc/sdk 0.2.0 → 0.2.1

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +9 -2
  2. package/README.md +32 -22
  3. package/dist/experimental/experimental.d.ts +6 -2
  4. package/dist/experimental/experimental.d.ts.map +1 -1
  5. package/dist/experimental/experimental.js +9 -3
  6. package/dist/experimental/index.d.ts +2 -2
  7. package/dist/experimental/index.d.ts.map +1 -1
  8. package/dist/experimental/index.js +6 -5
  9. package/dist/experimental/object/api.d.ts +11 -0
  10. package/dist/experimental/object/api.d.ts.map +1 -0
  11. package/dist/experimental/object/api.js +44 -0
  12. package/dist/experimental/object/client.d.ts +21 -0
  13. package/dist/experimental/object/client.d.ts.map +1 -0
  14. package/dist/experimental/object/client.js +127 -0
  15. package/dist/experimental/object/index.d.ts +5 -0
  16. package/dist/experimental/object/index.d.ts.map +1 -0
  17. package/dist/experimental/object/index.js +8 -0
  18. package/dist/experimental/object/types.d.ts +49 -0
  19. package/dist/experimental/object/types.d.ts.map +1 -0
  20. package/dist/generated/schema.d.ts +131 -3
  21. package/dist/generated/schema.d.ts.map +1 -1
  22. package/dist/workflows/uds.d.ts.map +1 -1
  23. package/dist/workflows/uds.js +26 -51
  24. package/examples/client/main.ts +1 -1
  25. package/examples/client/package-lock.json +4 -4
  26. package/examples/client/package.json +1 -1
  27. package/examples/task/main.ts +1 -1
  28. package/examples/task/package-lock.json +5 -6
  29. package/examples/task/package.json +1 -1
  30. package/package.json +9 -8
  31. package/src/errors.test.ts +75 -0
  32. package/src/experimental/experimental.ts +30 -7
  33. package/src/experimental/index.ts +18 -18
  34. package/src/experimental/{blob → object}/api.ts +7 -7
  35. package/src/experimental/object/client.test.ts +138 -0
  36. package/src/experimental/{blob → object}/client.ts +57 -57
  37. package/src/experimental/object/index.ts +22 -0
  38. package/src/experimental/object/types.test.ts +87 -0
  39. package/src/experimental/{blob → object}/types.ts +30 -30
  40. package/src/generated/schema.ts +217 -9
  41. package/src/utils/get-base-url.test.ts +58 -0
  42. package/src/workflows/client/client.test.ts +68 -0
  43. package/src/workflows/types.test.ts +52 -0
  44. package/src/workflows/uds.ts +29 -69
  45. package/tsconfig.json +1 -1
  46. package/{vite.config.ts → vitest.config.ts} +1 -0
  47. package/dist/workflows/client/errors.d.ts +0 -25
  48. package/dist/workflows/client/errors.d.ts.map +0 -1
  49. package/dist/workflows/client/errors.js +0 -56
  50. package/dist/workflows/client/schema.d.ts +0 -9322
  51. package/dist/workflows/client/schema.d.ts.map +0 -1
  52. package/dist/workflows/client/workflows.d.ts +0 -15
  53. package/dist/workflows/client/workflows.d.ts.map +0 -1
  54. package/dist/workflows/client/workflows.js +0 -63
  55. package/src/experimental/blob/index.ts +0 -22
  56. /package/dist/{workflows/client/schema.js → experimental/object/types.js} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"uds.d.ts","sourceRoot":"","sources":["../../src/workflows/uds.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,gBAAgB,EAEhB,wBAAwB,EAIxB,YAAY,EACb,MAAM,YAAY,CAAC;AAKpB,qBAAa,SAAS;IACR,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAAV,UAAU,EAAE,MAAM;IAKzC,QAAQ,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAI3C,OAAO,CAAC,iBAAiB;IAyBnB,YAAY,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO1D,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAa3D,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,CAAC;IAUtE,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YAa3C,OAAO;CAiFtB"}
1
+ {"version":3,"file":"uds.d.ts","sourceRoot":"","sources":["../../src/workflows/uds.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEV,gBAAgB,EAEhB,wBAAwB,EAIxB,YAAY,EACb,MAAM,YAAY,CAAC;AAKpB,qBAAa,SAAS;IACR,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAAV,UAAU,EAAE,MAAM;IAKzC,QAAQ,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAI3C,OAAO,CAAC,iBAAiB;IAyBnB,YAAY,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO1D,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAa3D,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,CAAC;IAUtE,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YAa3C,OAAO;CA2CtB"}
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.UDSClient = void 0;
4
- const node_net_1 = require("node:net");
7
+ const node_http_1 = __importDefault(require("node:http"));
5
8
  const version_js_1 = require("../version.js");
6
- const CONTENT_LENGTH_REGEX = /Content-Length:\s*(\d+)/i;
7
9
  class UDSClient {
8
10
  constructor(socketPath) {
9
11
  this.socketPath = socketPath;
@@ -59,66 +61,39 @@ class UDSClient {
59
61
  }
60
62
  async request(path, method, body) {
61
63
  return new Promise((resolve, reject) => {
62
- const client = (0, node_net_1.createConnection)({ path: this.socketPath }, () => {
63
- const bodyStr = body ? JSON.stringify(body) : "";
64
- const userAgent = (0, version_js_1.getUserAgent)();
65
- const request = `${method} ${path} HTTP/1.1\r\nHost: unix\r\nContent-Length: ${bodyStr.length}\r\nContent-Type: application/json\r\nUser-Agent: ${userAgent}\r\n\r\n${bodyStr}`;
66
- client.write(request);
67
- });
68
- let data = "";
69
- let contentLength = null;
70
- let headersParsed = false;
71
- let bodyStartIndex = -1;
72
- client.on("data", (chunk) => {
73
- data += chunk.toString();
74
- if (!headersParsed) {
75
- const headerEndIndex = data.indexOf("\r\n\r\n");
76
- if (headerEndIndex !== -1) {
77
- headersParsed = true;
78
- bodyStartIndex = headerEndIndex + 4;
79
- const headers = data.substring(0, headerEndIndex);
80
- const contentLengthMatch = headers.match(CONTENT_LENGTH_REGEX);
81
- if (contentLengthMatch) {
82
- contentLength = Number.parseInt(contentLengthMatch[1], 10);
83
- }
84
- }
64
+ const req = node_http_1.default.request({
65
+ socketPath: this.socketPath,
66
+ path: path,
67
+ method: method,
68
+ headers: {
69
+ "Content-Length": body ? JSON.stringify(body).length : 0,
70
+ "Content-Type": "application/json",
71
+ "User-Agent": (0, version_js_1.getUserAgent)(),
72
+ },
73
+ }, async (res) => {
74
+ const chunks = [];
75
+ for await (const chunk of res)
76
+ chunks.push(chunk);
77
+ const responseBody = Buffer.concat(chunks).toString();
78
+ if (res.statusCode && res.statusCode >= 400) {
79
+ reject(new Error(`HTTP ${res.statusCode}: ${responseBody}`));
80
+ return;
85
81
  }
86
- if (headersParsed && contentLength !== null) {
87
- const bodyReceived = data.length - bodyStartIndex;
88
- if (bodyReceived >= contentLength) {
89
- client.end();
90
- }
82
+ if (responseBody.length === 0) {
83
+ resolve(undefined);
84
+ return;
91
85
  }
92
- });
93
- client.on("end", () => {
94
86
  try {
95
- const lines = data.split("\r\n");
96
- const statusLine = lines[0];
97
- const statusCode = Number.parseInt(statusLine.split(" ")[1], 10);
98
- if (statusCode >= 400) {
99
- reject(new Error(`HTTP ${statusCode}: ${data}`));
100
- return;
101
- }
102
- const emptyLineIndex = lines.indexOf("");
103
- if (emptyLineIndex === -1) {
104
- resolve(undefined);
105
- return;
106
- }
107
- const bodyLines = lines.slice(emptyLineIndex + 1);
108
- const responseBody = bodyLines.join("\r\n").trim();
109
- if (!responseBody) {
110
- resolve(undefined);
111
- return;
112
- }
113
87
  resolve(JSON.parse(responseBody));
114
88
  }
115
89
  catch (error) {
116
90
  reject(error);
117
91
  }
118
92
  });
119
- client.on("error", (error) => {
93
+ req.on("error", (error) => {
120
94
  reject(error);
121
95
  });
96
+ req.end(body ? JSON.stringify(body) : "");
122
97
  });
123
98
  }
124
99
  }
@@ -1,4 +1,4 @@
1
- import { Render, ServerError } from "@render/sdk";
1
+ import { Render, ServerError } from "@renderinc/sdk";
2
2
 
3
3
  /**
4
4
  * Example: Using the REST API client to run tasks
@@ -8,7 +8,7 @@
8
8
  "name": "render-sdk-client-example",
9
9
  "version": "1.0.0",
10
10
  "dependencies": {
11
- "@render/sdk": "file:../.."
11
+ "@renderinc/sdk": "file:../.."
12
12
  },
13
13
  "devDependencies": {
14
14
  "@types/node": "22.19.0",
@@ -16,7 +16,7 @@
16
16
  }
17
17
  },
18
18
  "../..": {
19
- "name": "@render/sdk",
19
+ "name": "@renderinc/sdk",
20
20
  "version": "0.2.0",
21
21
  "license": "MIT",
22
22
  "dependencies": {
@@ -28,7 +28,7 @@
28
28
  "@types/node": "^20.0.0",
29
29
  "openapi-typescript": "^7.10.1",
30
30
  "typescript": "^5.9.3",
31
- "vitest": "^1.0.0"
31
+ "vitest": "^4.0.18"
32
32
  },
33
33
  "engines": {
34
34
  "node": ">=18.0.0"
@@ -476,7 +476,7 @@
476
476
  "node": ">=18"
477
477
  }
478
478
  },
479
- "node_modules/@render/sdk": {
479
+ "node_modules/@renderinc/sdk": {
480
480
  "resolved": "../..",
481
481
  "link": true
482
482
  },
@@ -7,7 +7,7 @@
7
7
  "start": "tsx main.ts"
8
8
  },
9
9
  "dependencies": {
10
- "@render/sdk": "file:../.."
10
+ "@renderinc/sdk": "file:../.."
11
11
  },
12
12
  "devDependencies": {
13
13
  "@types/node": "22.19.0",
@@ -1,4 +1,4 @@
1
- import { task } from "@render/sdk/workflows";
1
+ import { task } from "@renderinc/sdk/workflows";
2
2
 
3
3
  /**
4
4
  * Simple task that squares a number
@@ -9,15 +9,15 @@
9
9
  "version": "1.0.0",
10
10
  "hasInstallScript": true,
11
11
  "dependencies": {
12
- "@render/sdk": "file:../.."
12
+ "@renderinc/sdk": "file:../.."
13
13
  },
14
14
  "devDependencies": {
15
15
  "tsx": "^4.0.0"
16
16
  }
17
17
  },
18
18
  "../..": {
19
- "name": "@render/sdk",
20
- "version": "0.1.0",
19
+ "name": "@renderinc/sdk",
20
+ "version": "0.2.0",
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
23
  "eventsource": "^4.0.0",
@@ -25,11 +25,10 @@
25
25
  },
26
26
  "devDependencies": {
27
27
  "@biomejs/biome": "2.3.8",
28
- "@types/eventsource": "^3.0.0",
29
28
  "@types/node": "^20.0.0",
30
29
  "openapi-typescript": "^7.10.1",
31
30
  "typescript": "^5.9.3",
32
- "vitest": "^1.0.0"
31
+ "vitest": "^4.0.18"
33
32
  },
34
33
  "engines": {
35
34
  "node": ">=18.0.0"
@@ -477,7 +476,7 @@
477
476
  "node": ">=18"
478
477
  }
479
478
  },
480
- "node_modules/@render/sdk": {
479
+ "node_modules/@renderinc/sdk": {
481
480
  "resolved": "../..",
482
481
  "link": true
483
482
  },
@@ -8,7 +8,7 @@
8
8
  "start": "tsx main.ts"
9
9
  },
10
10
  "dependencies": {
11
- "@render/sdk": "file:../.."
11
+ "@renderinc/sdk": "file:../.."
12
12
  },
13
13
  "devDependencies": {
14
14
  "tsx": "^4.0.0"
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@renderinc/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Render SDK for TypeScript",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "tsc -p tsconfig.build.json",
9
9
  "test": "vitest",
10
+ "typecheck": "tsc --noEmit",
10
11
  "lint": "biome lint",
11
12
  "lint:fix": "biome lint --write",
12
13
  "format": "biome format --write",
@@ -27,7 +28,7 @@
27
28
  "@types/node": "^20.0.0",
28
29
  "openapi-typescript": "^7.10.1",
29
30
  "typescript": "^5.9.3",
30
- "vitest": "^1.0.0"
31
+ "vitest": "^4.0.18"
31
32
  },
32
33
  "dependencies": {
33
34
  "eventsource": "^4.0.0",
@@ -38,19 +39,19 @@
38
39
  },
39
40
  "exports": {
40
41
  ".": {
42
+ "types": "./dist/index.d.ts",
41
43
  "import": "./dist/index.js",
42
- "require": "./dist/index.js",
43
- "types": "./dist/index.d.ts"
44
+ "require": "./dist/index.js"
44
45
  },
45
46
  "./workflows": {
47
+ "types": "./dist/workflows/index.d.ts",
46
48
  "import": "./dist/workflows/index.js",
47
- "require": "./dist/workflows/index.js",
48
- "types": "./dist/workflows/index.d.ts"
49
+ "require": "./dist/workflows/index.js"
49
50
  },
50
51
  "./experimental": {
52
+ "types": "./dist/experimental/index.d.ts",
51
53
  "import": "./dist/experimental/index.js",
52
- "require": "./dist/experimental/index.js",
53
- "types": "./dist/experimental/index.d.ts"
54
+ "require": "./dist/experimental/index.js"
54
55
  }
55
56
  }
56
57
  }
@@ -0,0 +1,75 @@
1
+ import {
2
+ AbortError,
3
+ ClientError,
4
+ RenderError,
5
+ ServerError,
6
+ TaskRunError,
7
+ TimeoutError,
8
+ } from "./errors.js";
9
+
10
+ describe("errors", () => {
11
+ describe("RenderError", () => {
12
+ it("has correct name and message", () => {
13
+ const err = new RenderError("test message");
14
+ expect(err.name).toBe("RenderError");
15
+ expect(err.message).toBe("test message");
16
+ expect(err instanceof Error).toBe(true);
17
+ expect(err instanceof RenderError).toBe(true);
18
+ });
19
+ });
20
+
21
+ describe("TaskRunError", () => {
22
+ it("has correct name and properties", () => {
23
+ const err = new TaskRunError("task failed", "run-123", "internal error");
24
+ expect(err.name).toBe("TaskRunError");
25
+ expect(err.message).toBe("task failed");
26
+ expect(err.taskRunId).toBe("run-123");
27
+ expect(err.taskError).toBe("internal error");
28
+ expect(err instanceof RenderError).toBe(true);
29
+ });
30
+
31
+ it("works with optional properties", () => {
32
+ const err = new TaskRunError("task failed");
33
+ expect(err.taskRunId).toBeUndefined();
34
+ expect(err.taskError).toBeUndefined();
35
+ });
36
+ });
37
+
38
+ describe("ClientError", () => {
39
+ it("has correct name and properties", () => {
40
+ const err = new ClientError("not found", 404, { detail: "missing" });
41
+ expect(err.name).toBe("ClientError");
42
+ expect(err.statusCode).toBe(404);
43
+ expect(err.response).toEqual({ detail: "missing" });
44
+ expect(err instanceof RenderError).toBe(true);
45
+ });
46
+ });
47
+
48
+ describe("ServerError", () => {
49
+ it("has correct name and properties", () => {
50
+ const err = new ServerError("server error", 500, { detail: "crash" });
51
+ expect(err.name).toBe("ServerError");
52
+ expect(err.statusCode).toBe(500);
53
+ expect(err.response).toEqual({ detail: "crash" });
54
+ expect(err instanceof RenderError).toBe(true);
55
+ });
56
+ });
57
+
58
+ describe("TimeoutError", () => {
59
+ it("has correct name and inherits from RenderError", () => {
60
+ const err = new TimeoutError("request timed out");
61
+ expect(err.name).toBe("TimeoutError");
62
+ expect(err.message).toBe("request timed out");
63
+ expect(err instanceof RenderError).toBe(true);
64
+ });
65
+ });
66
+
67
+ describe("AbortError", () => {
68
+ it("has correct name and fixed message", () => {
69
+ const err = new AbortError();
70
+ expect(err.name).toBe("AbortError");
71
+ expect(err.message).toBe("The operation was aborted.");
72
+ expect(err instanceof Error).toBe(true);
73
+ });
74
+ });
75
+ });
@@ -1,6 +1,29 @@
1
1
  import type { Client } from "openapi-fetch";
2
2
  import type { paths } from "../generated/schema.js";
3
- import { BlobClient } from "./blob/client.js";
3
+ import { ObjectClient } from "./object/client.js";
4
+
5
+ /**
6
+ * StorageClient provides access to experimental storage features
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * // Access object storage
11
+ * await render.experimental.storage.objects.put({
12
+ * ownerId: "tea-xxxxx",
13
+ * region: "oregon",
14
+ * key: "file.png",
15
+ * data: buffer
16
+ * });
17
+ * ```
18
+ */
19
+ export class StorageClient {
20
+ /** Object storage client for managing binary objects */
21
+ public readonly objects: ObjectClient;
22
+
23
+ constructor(apiClient: Client<paths>) {
24
+ this.objects = new ObjectClient(apiClient);
25
+ }
26
+ }
4
27
 
5
28
  /**
6
29
  * ExperimentalClient provides access to experimental Render SDK features
@@ -10,12 +33,12 @@ import { BlobClient } from "./blob/client.js";
10
33
  *
11
34
  * @example
12
35
  * ```typescript
13
- * import { Render } from '@render/sdk';
36
+ * import { Render } from '@renderinc/sdk';
14
37
  *
15
38
  * const render = new Render();
16
39
  *
17
- * // Access experimental blob storage
18
- * await render.experimental.blob.put({
40
+ * // Access experimental object storage
41
+ * await render.experimental.storage.objects.put({
19
42
  * ownerId: "tea-xxxxx",
20
43
  * region: "oregon",
21
44
  * key: "file.png",
@@ -24,10 +47,10 @@ import { BlobClient } from "./blob/client.js";
24
47
  * ```
25
48
  */
26
49
  export class ExperimentalClient {
27
- /** Blob storage client for managing binary objects */
28
- public readonly blob: BlobClient;
50
+ /** Storage client for managing storage features */
51
+ public readonly storage: StorageClient;
29
52
 
30
53
  constructor(apiClient: Client<paths>) {
31
- this.blob = new BlobClient(apiClient);
54
+ this.storage = new StorageClient(apiClient);
32
55
  }
33
56
  }
@@ -1,24 +1,24 @@
1
1
  // Experimental client
2
2
 
3
- // Blob storage exports
3
+ export { ExperimentalClient, StorageClient } from "./experimental.js";
4
+ // Object storage exports
4
5
  export {
5
- BlobApi,
6
- BlobClient,
7
- type BlobData,
8
- type BlobIdentifier,
9
- type BlobScope,
10
- type DeleteBlobInput,
11
- type GetBlobInput,
6
+ type DeleteObjectInput,
7
+ type GetObjectInput,
8
+ ObjectApi,
9
+ ObjectClient,
10
+ type ObjectData,
11
+ type ObjectIdentifier,
12
+ type ObjectScope,
12
13
  type PresignedDownloadUrl,
13
14
  type PresignedUploadUrl,
14
- type PutBlobInput,
15
- type PutBlobInputBuffer,
16
- type PutBlobInputStream,
17
- type PutBlobResult,
15
+ type PutObjectInput,
16
+ type PutObjectInputBuffer,
17
+ type PutObjectInputStream,
18
+ type PutObjectResult,
18
19
  Region,
19
- ScopedBlobClient,
20
- type ScopedDeleteBlobInput,
21
- type ScopedGetBlobInput,
22
- type ScopedPutBlobInput,
23
- } from "./blob/index.js";
24
- export { ExperimentalClient } from "./experimental.js";
20
+ type ScopedDeleteObjectInput,
21
+ type ScopedGetObjectInput,
22
+ ScopedObjectClient,
23
+ type ScopedPutObjectInput,
24
+ } from "./object/index.js";
@@ -4,23 +4,23 @@ import type { paths } from "../../generated/schema.js";
4
4
  import type { PresignedDownloadUrl, PresignedUploadUrl, Region } from "./types.js";
5
5
 
6
6
  /**
7
- * Layer 2: Typed Blob API Client
7
+ * Layer 2: Typed Object API Client
8
8
  *
9
9
  * Provides idiomatic TypeScript wrapper around the raw OpenAPI client.
10
10
  * Handles presigned URL flow but still exposes the two-step nature
11
11
  * (get URL, then upload/download). Useful for advanced use cases
12
12
  * requiring fine-grained control.
13
13
  */
14
- export class BlobApi {
14
+ export class ObjectApi {
15
15
  constructor(private readonly apiClient: Client<paths>) {}
16
16
 
17
17
  /**
18
- * Get a presigned URL for uploading a blob
18
+ * Get a presigned URL for uploading an object
19
19
  *
20
20
  * @param ownerId - Owner ID (workspace team ID)
21
21
  * @param region - Storage region
22
22
  * @param key - Object key (path)
23
- * @param sizeBytes - Size of the blob in bytes
23
+ * @param sizeBytes - Size of the object in bytes
24
24
  * @returns Presigned upload URL with expiration and size limit
25
25
  */
26
26
  async getUploadUrl(
@@ -46,7 +46,7 @@ export class BlobApi {
46
46
  }
47
47
 
48
48
  /**
49
- * Get a presigned URL for downloading a blob
49
+ * Get a presigned URL for downloading an object
50
50
  *
51
51
  * @param ownerId - Owner ID (workspace team ID)
52
52
  * @param region - Storage region
@@ -73,7 +73,7 @@ export class BlobApi {
73
73
  }
74
74
 
75
75
  /**
76
- * Delete a blob
76
+ * Delete an object
77
77
  *
78
78
  * @param ownerId - Owner ID (workspace team ID)
79
79
  * @param region - Storage region
@@ -85,7 +85,7 @@ export class BlobApi {
85
85
  });
86
86
 
87
87
  if (error) {
88
- throw new RenderError(`Failed to delete blob: ${error.message || "Unknown error"}`);
88
+ throw new RenderError(`Failed to delete object: ${error.message || "Unknown error"}`);
89
89
  }
90
90
  }
91
91
  }
@@ -0,0 +1,138 @@
1
+ import { Readable } from "node:stream";
2
+ import type { Client } from "openapi-fetch";
3
+ import { RenderError } from "../../errors.js";
4
+ import type { paths } from "../../generated/schema.js";
5
+ import { ObjectClient } from "./client.js";
6
+ import type { PutObjectInput } from "./types.js";
7
+
8
+ describe("ObjectClient", () => {
9
+ describe("resolveSize (via put validation)", () => {
10
+ // Test resolveSize indirectly by calling put() which will fail
11
+ // at the API call stage, but size validation happens first.
12
+ const putMock = vi.fn().mockRejectedValue(new Error("should not reach API"));
13
+ const mockApiClient = { PUT: putMock } as unknown as Client<paths>;
14
+
15
+ const client = new ObjectClient(mockApiClient);
16
+
17
+ it("auto-calculates Buffer size", async () => {
18
+ const buffer = Buffer.from("hello");
19
+ putMock.mockResolvedValueOnce({
20
+ data: { url: "http://test" },
21
+ error: null,
22
+ });
23
+
24
+ // Will fail at fetch, but that's after size validation
25
+ await expect(
26
+ client.put({
27
+ ownerId: "tea-test",
28
+ region: "oregon",
29
+ key: "test.txt",
30
+ data: buffer,
31
+ }),
32
+ ).rejects.toThrow(); // fetch not available in test
33
+
34
+ expect(putMock).toHaveBeenCalledWith(
35
+ "/blobs/{ownerId}/{region}/{key}",
36
+ expect.objectContaining({
37
+ body: { sizeBytes: 5 },
38
+ }),
39
+ );
40
+ });
41
+
42
+ it("auto-calculates Uint8Array size", async () => {
43
+ const arr = new Uint8Array([1, 2, 3, 4]);
44
+ putMock.mockResolvedValueOnce({
45
+ data: { url: "http://test" },
46
+ error: null,
47
+ });
48
+
49
+ await expect(
50
+ client.put({
51
+ ownerId: "tea-test",
52
+ region: "oregon",
53
+ key: "test.bin",
54
+ data: arr,
55
+ }),
56
+ ).rejects.toThrow();
57
+
58
+ expect(putMock).toHaveBeenCalledWith(
59
+ "/blobs/{ownerId}/{region}/{key}",
60
+ expect.objectContaining({
61
+ body: { sizeBytes: 4 },
62
+ }),
63
+ );
64
+ });
65
+
66
+ it("throws on size mismatch for Buffer", async () => {
67
+ const buffer = Buffer.from("hello");
68
+ await expect(
69
+ client.put({
70
+ ownerId: "tea-test",
71
+ region: "oregon",
72
+ key: "test.txt",
73
+ data: buffer,
74
+ size: 10,
75
+ }),
76
+ ).rejects.toThrow(RenderError);
77
+ await expect(
78
+ client.put({
79
+ ownerId: "tea-test",
80
+ region: "oregon",
81
+ key: "test.txt",
82
+ data: buffer,
83
+ size: 10,
84
+ }),
85
+ ).rejects.toThrow("Size mismatch");
86
+ });
87
+
88
+ it("requires size for stream input", async () => {
89
+ const stream = Readable.from(["hello"]);
90
+ const invalidInput = {
91
+ ownerId: "tea-test",
92
+ region: "oregon",
93
+ key: "test.txt",
94
+ data: stream,
95
+ // Intentionally omit size to test validation (invalid at runtime)
96
+ } as unknown as PutObjectInput;
97
+ await expect(client.put(invalidInput)).rejects.toThrow(RenderError);
98
+ await expect(client.put(invalidInput)).rejects.toThrow("Size is required");
99
+ });
100
+
101
+ it("requires size for string input", async () => {
102
+ await expect(
103
+ client.put({
104
+ ownerId: "tea-test",
105
+ region: "oregon",
106
+ key: "test.txt",
107
+ data: "hello",
108
+ }),
109
+ ).rejects.toThrow("Size is required");
110
+ });
111
+
112
+ it("throws on zero size", async () => {
113
+ const stream = Readable.from(["hello"]);
114
+ await expect(
115
+ client.put({
116
+ ownerId: "tea-test",
117
+ region: "oregon",
118
+ key: "test.txt",
119
+ data: stream,
120
+ size: 0,
121
+ }),
122
+ ).rejects.toThrow("Size must be a positive integer");
123
+ });
124
+
125
+ it("throws on negative size", async () => {
126
+ const stream = Readable.from(["hello"]);
127
+ await expect(
128
+ client.put({
129
+ ownerId: "tea-test",
130
+ region: "oregon",
131
+ key: "test.txt",
132
+ data: stream,
133
+ size: -1,
134
+ }),
135
+ ).rejects.toThrow("Size must be a positive integer");
136
+ });
137
+ });
138
+ });