@terrascale/sdk 0.0.7 → 0.0.8
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/package.json +2 -1
- package/sdk-file-storage-route-manifest.json +146 -0
- package/src/index.js +10 -0
- package/src/v1/file-storage-routes.js +17 -0
- package/src/v1/file-storage.js +163 -0
- package/src/v1/routes.js +3 -2
- package/src/v1/terrabase-v1.js +6 -0
- package/src/v1/usage.js +1 -1
- package/types/index.d.ts +9 -0
- package/types/v1/file-storage-routes.d.ts +3 -0
- package/types/v1/file-storage.d.ts +98 -0
- package/types/v1/routes.d.ts +1 -1
- package/types/v1/terrabase-v1.d.ts +3 -0
- package/types/v1/usage.d.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terrascale/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "JavaScript SDK, typed with JSDoc, for project-scoped C# TerraBase v1 and hosted TerraScale identity.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
"!types/integration",
|
|
84
84
|
"sdk-v1-route-manifest.json",
|
|
85
85
|
"sdk-public-v1-route-manifest.json",
|
|
86
|
+
"sdk-file-storage-route-manifest.json",
|
|
86
87
|
"README.md",
|
|
87
88
|
"CHANGELOG.md",
|
|
88
89
|
"LICENSE"
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
{
|
|
2
|
+
"producerRevision": "1c94fe262795ea87f55248989e04f5b93f2f40bf",
|
|
3
|
+
"routes": [
|
|
4
|
+
{
|
|
5
|
+
"operation": "createFileBucket",
|
|
6
|
+
"method": "POST",
|
|
7
|
+
"pathTemplate": "/v1/projects/{project}/buckets",
|
|
8
|
+
"permission": "documents:write",
|
|
9
|
+
"requestMediaTypes": [
|
|
10
|
+
"application/json"
|
|
11
|
+
],
|
|
12
|
+
"successStatuses": [
|
|
13
|
+
201
|
|
14
|
+
],
|
|
15
|
+
"responseMediaType": "application/json",
|
|
16
|
+
"idempotencyKey": "none",
|
|
17
|
+
"replaySafe": false,
|
|
18
|
+
"contract": "docs/file-buckets.md",
|
|
19
|
+
"contractText": []
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"operation": "listFileBuckets",
|
|
23
|
+
"method": "GET",
|
|
24
|
+
"pathTemplate": "/v1/projects/{project}/buckets",
|
|
25
|
+
"permission": "documents:read",
|
|
26
|
+
"requestMediaTypes": [],
|
|
27
|
+
"successStatuses": [
|
|
28
|
+
200
|
|
29
|
+
],
|
|
30
|
+
"responseMediaType": "application/json",
|
|
31
|
+
"idempotencyKey": "none",
|
|
32
|
+
"replaySafe": true,
|
|
33
|
+
"contract": "docs/file-buckets.md",
|
|
34
|
+
"contractText": []
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"operation": "setFileBucketRules",
|
|
38
|
+
"method": "PUT",
|
|
39
|
+
"pathTemplate": "/v1/projects/{project}/buckets/{bucket}/rules",
|
|
40
|
+
"permission": "documents:write",
|
|
41
|
+
"requestMediaTypes": [
|
|
42
|
+
"application/json"
|
|
43
|
+
],
|
|
44
|
+
"successStatuses": [
|
|
45
|
+
200
|
|
46
|
+
],
|
|
47
|
+
"responseMediaType": "application/json",
|
|
48
|
+
"idempotencyKey": "none",
|
|
49
|
+
"replaySafe": false,
|
|
50
|
+
"contract": "docs/file-buckets.md",
|
|
51
|
+
"contractText": []
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"operation": "deleteFileBucket",
|
|
55
|
+
"method": "DELETE",
|
|
56
|
+
"pathTemplate": "/v1/projects/{project}/buckets/{bucket}",
|
|
57
|
+
"permission": "documents:write",
|
|
58
|
+
"requestMediaTypes": [],
|
|
59
|
+
"successStatuses": [
|
|
60
|
+
204
|
|
61
|
+
],
|
|
62
|
+
"responseMediaType": "",
|
|
63
|
+
"idempotencyKey": "none",
|
|
64
|
+
"replaySafe": false,
|
|
65
|
+
"contract": "docs/file-buckets.md",
|
|
66
|
+
"contractText": []
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"operation": "createFileUpload",
|
|
70
|
+
"method": "POST",
|
|
71
|
+
"pathTemplate": "/v1/projects/{project}/buckets/{bucket}/files/upload",
|
|
72
|
+
"permission": "documents:write",
|
|
73
|
+
"requestMediaTypes": [
|
|
74
|
+
"application/json"
|
|
75
|
+
],
|
|
76
|
+
"successStatuses": [
|
|
77
|
+
201
|
|
78
|
+
],
|
|
79
|
+
"responseMediaType": "application/json",
|
|
80
|
+
"idempotencyKey": "none",
|
|
81
|
+
"replaySafe": false,
|
|
82
|
+
"contract": "docs/file-buckets.md",
|
|
83
|
+
"contractText": []
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"operation": "completeFileUpload",
|
|
87
|
+
"method": "POST",
|
|
88
|
+
"pathTemplate": "/v1/projects/{project}/buckets/{bucket}/files/{file}/complete",
|
|
89
|
+
"permission": "documents:write",
|
|
90
|
+
"requestMediaTypes": [],
|
|
91
|
+
"successStatuses": [
|
|
92
|
+
200
|
|
93
|
+
],
|
|
94
|
+
"responseMediaType": "application/json",
|
|
95
|
+
"idempotencyKey": "none",
|
|
96
|
+
"replaySafe": false,
|
|
97
|
+
"contract": "docs/file-buckets.md",
|
|
98
|
+
"contractText": []
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"operation": "listBucketFiles",
|
|
102
|
+
"method": "GET",
|
|
103
|
+
"pathTemplate": "/v1/projects/{project}/buckets/{bucket}/files",
|
|
104
|
+
"permission": "documents:read",
|
|
105
|
+
"requestMediaTypes": [],
|
|
106
|
+
"successStatuses": [
|
|
107
|
+
200
|
|
108
|
+
],
|
|
109
|
+
"responseMediaType": "application/json",
|
|
110
|
+
"idempotencyKey": "none",
|
|
111
|
+
"replaySafe": true,
|
|
112
|
+
"contract": "docs/file-buckets.md",
|
|
113
|
+
"contractText": []
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"operation": "createFileDownload",
|
|
117
|
+
"method": "POST",
|
|
118
|
+
"pathTemplate": "/v1/projects/{project}/buckets/{bucket}/files/{file}/download",
|
|
119
|
+
"permission": "documents:read",
|
|
120
|
+
"requestMediaTypes": [],
|
|
121
|
+
"successStatuses": [
|
|
122
|
+
200
|
|
123
|
+
],
|
|
124
|
+
"responseMediaType": "application/json",
|
|
125
|
+
"idempotencyKey": "none",
|
|
126
|
+
"replaySafe": false,
|
|
127
|
+
"contract": "docs/file-buckets.md",
|
|
128
|
+
"contractText": []
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"operation": "deleteBucketFile",
|
|
132
|
+
"method": "DELETE",
|
|
133
|
+
"pathTemplate": "/v1/projects/{project}/buckets/{bucket}/files/{file}",
|
|
134
|
+
"permission": "documents:write",
|
|
135
|
+
"requestMediaTypes": [],
|
|
136
|
+
"successStatuses": [
|
|
137
|
+
204
|
|
138
|
+
],
|
|
139
|
+
"responseMediaType": "",
|
|
140
|
+
"idempotencyKey": "none",
|
|
141
|
+
"replaySafe": false,
|
|
142
|
+
"contract": "docs/file-buckets.md",
|
|
143
|
+
"contractText": []
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
}
|
package/src/index.js
CHANGED
|
@@ -161,3 +161,13 @@ export { createTerraBaseSystemClient, V1Health, V1Platform } from "./v1/system.j
|
|
|
161
161
|
/** @typedef {import("./v1/shape-manifest.js").V1ShapeManifestRequest} V1ShapeManifestRequest */
|
|
162
162
|
/** @typedef {import("./v1/shape-reader.js").V1ShapeKeyRequest} V1ShapeKeyRequest */
|
|
163
163
|
/** @typedef {import("./v1/shape-reader.js").V1ShapeKey} V1ShapeKey */
|
|
164
|
+
|
|
165
|
+
export { V1FileStorage, V1FileBucket } from "./v1/file-storage.js";
|
|
166
|
+
/** @typedef {import("./v1/file-storage.js").FileAccessPolicy} FileAccessPolicy */
|
|
167
|
+
/** @typedef {import("./v1/file-storage.js").FileAccessRules} FileAccessRules */
|
|
168
|
+
/** @typedef {import("./v1/file-storage.js").FileBucketInfo} FileBucketInfo */
|
|
169
|
+
/** @typedef {import("./v1/file-storage.js").StoredFile} StoredFile */
|
|
170
|
+
/** @typedef {import("./v1/file-storage.js").SignedFileUrl} SignedFileUrl */
|
|
171
|
+
/** @typedef {import("./v1/file-storage.js").FileUploadRequest} FileUploadRequest */
|
|
172
|
+
/** @typedef {import("./v1/file-storage.js").FileUploadOptions} FileUploadOptions */
|
|
173
|
+
/** @typedef {import("./v1/file-storage.js").FileListOptions} FileListOptions */
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** @typedef {"createFileBucket" | "listFileBuckets" | "setFileBucketRules" | "deleteFileBucket" | "createFileUpload" | "completeFileUpload" | "listBucketFiles" | "createFileDownload" | "deleteBucketFile"} FileStorageOperation */
|
|
2
|
+
const buckets = "/v1/projects/{project}/buckets";
|
|
3
|
+
const bucket = `${buckets}/{bucket}`;
|
|
4
|
+
const files = `${bucket}/files`;
|
|
5
|
+
const file = `${files}/{file}`;
|
|
6
|
+
/** @type {readonly import("./routes.js").V1Route[]} */
|
|
7
|
+
export const fileStorageRoutes = Object.freeze(/** @type {import("./routes.js").V1Route[]} */ ([
|
|
8
|
+
{ operation: "createFileBucket", method: "POST", pathTemplate: buckets, permission: "documents:write", requestMediaTypes: ["application/json"], successStatuses: [201] },
|
|
9
|
+
{ operation: "listFileBuckets", method: "GET", pathTemplate: buckets, permission: "documents:read", requestMediaTypes: [], successStatuses: [200] },
|
|
10
|
+
{ operation: "setFileBucketRules", method: "PUT", pathTemplate: `${bucket}/rules`, permission: "documents:write", requestMediaTypes: ["application/json"], successStatuses: [200] },
|
|
11
|
+
{ operation: "deleteFileBucket", method: "DELETE", pathTemplate: bucket, permission: "documents:write", requestMediaTypes: [], successStatuses: [204] },
|
|
12
|
+
{ operation: "createFileUpload", method: "POST", pathTemplate: `${files}/upload`, permission: "documents:write", requestMediaTypes: ["application/json"], successStatuses: [201] },
|
|
13
|
+
{ operation: "completeFileUpload", method: "POST", pathTemplate: `${file}/complete`, permission: "documents:write", requestMediaTypes: [], successStatuses: [200] },
|
|
14
|
+
{ operation: "listBucketFiles", method: "GET", pathTemplate: files, permission: "documents:read", requestMediaTypes: [], successStatuses: [200] },
|
|
15
|
+
{ operation: "createFileDownload", method: "POST", pathTemplate: `${file}/download`, permission: "documents:read", requestMediaTypes: [], successStatuses: [200] },
|
|
16
|
+
{ operation: "deleteBucketFile", method: "DELETE", pathTemplate: file, permission: "documents:write", requestMediaTypes: [], successStatuses: [204] },
|
|
17
|
+
].map(route => Object.freeze({ ...route, responseMediaType: route.successStatuses.includes(204) ? "" : "application/json", idempotencyKey: "none", replaySafe: route.method === "GET", contract: "docs/file-buckets.md", contractText: [] }))));
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { array, boolean, malformed, object, string } from "./decode.js";
|
|
2
|
+
import { requireId } from "../ids.js";
|
|
3
|
+
import { assertJsonObject, stringifyExactJson } from "./json.js";
|
|
4
|
+
import { requireServiceUrl } from "./transport.js";
|
|
5
|
+
/** @import { V1Transport } from "./transport.js" */
|
|
6
|
+
/** @import { FetchLike } from "../config.js" */
|
|
7
|
+
/** @import { JsonObject, JsonValue } from "./json.js" */
|
|
8
|
+
/** @typedef {{ owner: boolean, public: boolean, roles: readonly string[] }} FileAccessPolicy */
|
|
9
|
+
/** @typedef {{ owner: string | null, read: FileAccessPolicy, write: FileAccessPolicy }} FileAccessRules */
|
|
10
|
+
/** @typedef {{ id: string, name: string, region: string, rules: FileAccessRules }} FileBucketInfo */
|
|
11
|
+
/** @typedef {{ id: string, name: string, contentType: string, size: number, sha256: string, metadata: JsonObject, status: string, uploadExpiresAt: string }} StoredFile */
|
|
12
|
+
/** @typedef {{ id: string, url: string, expiresAt: string, headers: Readonly<Record<string, string>> }} SignedFileUrl */
|
|
13
|
+
/** @typedef {{ name: string, contentType: string, size: number, sha256: string, metadata: JsonObject }} FileUploadRequest */
|
|
14
|
+
/** @typedef {{ signal?: AbortSignal | undefined }} FileRequestOptions */
|
|
15
|
+
/** @typedef {{ after?: string | undefined, signal?: AbortSignal | undefined }} FileListOptions */
|
|
16
|
+
/** @typedef {{ metadata: JsonObject, contentType?: string | undefined, signal?: AbortSignal | undefined }} FileUploadOptions */
|
|
17
|
+
const maxFileBytes = 100 * 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
/** @param {JsonValue | undefined} value @returns {FileAccessPolicy} */
|
|
20
|
+
function policy(value) {
|
|
21
|
+
const v = object(value, "file access policy");
|
|
22
|
+
return { owner: boolean(v.owner, "owner grant"), public: boolean(v.public, "public grant"), roles: array(v.roles, "roles").map(v => string(v, "role")) };
|
|
23
|
+
}
|
|
24
|
+
/** @param {JsonValue | undefined} value @returns {FileAccessRules} */
|
|
25
|
+
function rules(value) {
|
|
26
|
+
const v = object(value, "file access rules");
|
|
27
|
+
return { owner: v.owner === null ? null : string(v.owner, "owner pointer"), read: policy(v.read), write: policy(v.write) };
|
|
28
|
+
}
|
|
29
|
+
/** @param {JsonValue | undefined} value @returns {FileBucketInfo} */
|
|
30
|
+
function bucketInfo(value) {
|
|
31
|
+
const v = object(value, "file bucket");
|
|
32
|
+
return { id: requireId(v.id), name: string(v.name, "bucket name"), region: string(v.region, "bucket region"), rules: rules(v.rules) };
|
|
33
|
+
}
|
|
34
|
+
/** @param {JsonValue | undefined} value @returns {StoredFile} */
|
|
35
|
+
function storedFile(value) {
|
|
36
|
+
const v = object(value, "file");
|
|
37
|
+
if (typeof v.size !== "number" || !Number.isSafeInteger(v.size) || v.size < 1 || v.size > maxFileBytes) malformed("file size");
|
|
38
|
+
return { id: requireId(v.id), name: string(v.name, "file name"), contentType: string(v.contentType, "content type"), size: v.size,
|
|
39
|
+
sha256: string(v.sha256, "checksum"), metadata: object(v.metadata, "file metadata"), status: string(v.status, "file status"), uploadExpiresAt: string(v.uploadExpiresAt, "upload expiry") };
|
|
40
|
+
}
|
|
41
|
+
/** @param {JsonValue | undefined} value @returns {SignedFileUrl} */
|
|
42
|
+
function signed(value) {
|
|
43
|
+
const v = object(value, "signed file URL");
|
|
44
|
+
const url = string(v.url, "signed URL");
|
|
45
|
+
requireServiceUrl(url, "Signed file URL");
|
|
46
|
+
/** @type {Record<string, string>} */
|
|
47
|
+
const headers = {};
|
|
48
|
+
for (const [key, val] of Object.entries(object(v.headers, "signed headers"))) {
|
|
49
|
+
// Object-store capabilities must never carry the project's authorization or cookies.
|
|
50
|
+
if (!["content-type", "content-length", "x-amz-checksum-sha256"].includes(key.toLowerCase())) malformed("signed upload header");
|
|
51
|
+
headers[key] = string(val, "signed header value");
|
|
52
|
+
}
|
|
53
|
+
return { id: requireId(v.id), url, expiresAt: string(v.expiresAt, "signed URL expiry"), headers };
|
|
54
|
+
}
|
|
55
|
+
/** @param {FileListOptions} options @returns {URLSearchParams} */
|
|
56
|
+
function pageQuery(options) {
|
|
57
|
+
const query = new URLSearchParams();
|
|
58
|
+
if (options.after !== undefined) query.set("after", requireId(options.after));
|
|
59
|
+
return query;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Project file buckets, authenticated by the same token as documents. */
|
|
63
|
+
export class V1FileStorage {
|
|
64
|
+
/** @type {V1Transport} */ #transport;
|
|
65
|
+
/** @type {FetchLike} */ #fetch;
|
|
66
|
+
/** @param {V1Transport} transport @param {FetchLike} fetcher */
|
|
67
|
+
constructor(transport, fetcher) { this.#transport = transport; this.#fetch = fetcher; Object.freeze(this); }
|
|
68
|
+
/** @param {string} name @param {FileAccessRules} accessRules @param {FileRequestOptions} [options] @returns {Promise<FileBucketInfo>} */
|
|
69
|
+
async createBucket(name, accessRules, options = {}) {
|
|
70
|
+
return bucketInfo((await this.#transport.call("createFileBucket", {}, { body: stringifyExactJson({ name, rules: accessRules }, "file bucket"), contentType: "application/json", signal: options.signal })).value);
|
|
71
|
+
}
|
|
72
|
+
/** @param {FileListOptions} [options] @returns {Promise<{items: FileBucketInfo[], next: string | null}>} */
|
|
73
|
+
async listBuckets(options = {}) {
|
|
74
|
+
const v = object((await this.#transport.call("listFileBuckets", {}, { query: pageQuery(options), signal: options.signal })).value, "bucket page");
|
|
75
|
+
return { items: array(v.items, "bucket items").map(bucketInfo), next: v.next === null ? null : requireId(v.next) };
|
|
76
|
+
}
|
|
77
|
+
/** @param {string} id @returns {V1FileBucket} */
|
|
78
|
+
bucket(id) { return new V1FileBucket(this.#transport, this.#fetch, requireId(id)); }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Immutable files in one project bucket. */
|
|
82
|
+
export class V1FileBucket {
|
|
83
|
+
/** @type {V1Transport} */ #transport;
|
|
84
|
+
/** @type {FetchLike} */ #fetch;
|
|
85
|
+
/** @type {string} */ #bucket;
|
|
86
|
+
/** @param {V1Transport} transport @param {FetchLike} fetcher @param {string} bucket */
|
|
87
|
+
constructor(transport, fetcher, bucket) { this.#transport = transport; this.#fetch = fetcher; this.#bucket = requireId(bucket); Object.freeze(this); }
|
|
88
|
+
/** @param {FileAccessRules} accessRules @param {FileRequestOptions} [options] @returns {Promise<FileBucketInfo>} */
|
|
89
|
+
async setRules(accessRules, options = {}) {
|
|
90
|
+
return bucketInfo((await this.#transport.call("setFileBucketRules", { bucket: this.#bucket }, { body: stringifyExactJson({ rules: accessRules }, "file bucket rules"), contentType: "application/json", signal: options.signal })).value);
|
|
91
|
+
}
|
|
92
|
+
/** @param {FileRequestOptions} [options] @returns {Promise<void>} */
|
|
93
|
+
async delete(options = {}) { await this.#transport.call("deleteFileBucket", { bucket: this.#bucket }, { signal: options.signal }); }
|
|
94
|
+
/** @param {FileUploadRequest} request @param {FileRequestOptions} [options] @returns {Promise<SignedFileUrl>} */
|
|
95
|
+
async createUpload(request, options = {}) {
|
|
96
|
+
assertJsonObject(request.metadata, "file metadata");
|
|
97
|
+
if (!Number.isSafeInteger(request.size) || request.size < 1 || request.size > maxFileBytes) throw new TypeError("Files must contain 1–104857600 bytes.");
|
|
98
|
+
return signed((await this.#transport.call("createFileUpload", { bucket: this.#bucket }, { body: stringifyExactJson(request, "file upload"), contentType: "application/json", signal: options.signal })).value);
|
|
99
|
+
}
|
|
100
|
+
/** @param {string} id @param {FileRequestOptions} [options] @returns {Promise<StoredFile>} */
|
|
101
|
+
async completeUpload(id, options = {}) {
|
|
102
|
+
return storedFile((await this.#transport.call("completeFileUpload", { bucket: this.#bucket, file: requireId(id) }, { signal: options.signal })).value);
|
|
103
|
+
}
|
|
104
|
+
/** @param {FileListOptions} [options] @returns {Promise<{items: StoredFile[], next: string | null}>} */
|
|
105
|
+
async list(options = {}) {
|
|
106
|
+
const v = object((await this.#transport.call("listBucketFiles", { bucket: this.#bucket }, { query: pageQuery(options), signal: options.signal })).value, "file page");
|
|
107
|
+
return { items: array(v.items, "file items").map(storedFile), next: v.next === null ? null : requireId(v.next) };
|
|
108
|
+
}
|
|
109
|
+
/** @param {string} id @param {FileRequestOptions} [options] @returns {Promise<SignedFileUrl>} */
|
|
110
|
+
async downloadUrl(id, options = {}) {
|
|
111
|
+
return signed((await this.#transport.call("createFileDownload", { bucket: this.#bucket, file: requireId(id) }, { signal: options.signal })).value);
|
|
112
|
+
}
|
|
113
|
+
/** @param {string} id @param {FileRequestOptions} [options] @returns {Promise<void>} */
|
|
114
|
+
async deleteFile(id, options = {}) { await this.#transport.call("deleteBucketFile", { bucket: this.#bucket, file: requireId(id) }, { signal: options.signal }); }
|
|
115
|
+
/** Hashes a private byte snapshot, uploads it without project credentials, and verifies completion. No retries.
|
|
116
|
+
* @param {string} name @param {Uint8Array | Blob} input @param {FileUploadOptions} options @returns {Promise<StoredFile>} */
|
|
117
|
+
async upload(name, input, options) {
|
|
118
|
+
const size = input instanceof Blob ? input.size : input.byteLength;
|
|
119
|
+
if (size < 1 || size > maxFileBytes) throw new TypeError("Files must contain 1–104857600 bytes.");
|
|
120
|
+
const bytes = input instanceof Blob ? new Uint8Array(await input.arrayBuffer()) : new Uint8Array(input);
|
|
121
|
+
options.signal?.throwIfAborted();
|
|
122
|
+
const hash = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", bytes));
|
|
123
|
+
const sha256 = btoa(String.fromCharCode(...hash));
|
|
124
|
+
const capability = await this.createUpload({ name, contentType: options.contentType ?? "application/octet-stream", size, sha256, metadata: options.metadata }, options);
|
|
125
|
+
const headers = new Headers(capability.headers);
|
|
126
|
+
// Fetch computes Content-Length from the body; browsers prohibit setting it explicitly.
|
|
127
|
+
headers.delete("Content-Length");
|
|
128
|
+
/** @type {RequestInit & { duplex?: "half" }} */
|
|
129
|
+
const init = { method: "PUT", headers, body: bytes, credentials: "omit", redirect: "error", signal: options.signal ?? null };
|
|
130
|
+
if (typeof process !== "undefined" && process.release?.name === "node") {
|
|
131
|
+
// A one-shot stream prevents Node fetch from replaying the signed PUT on HTTP 421.
|
|
132
|
+
headers.set("Content-Length", String(size));
|
|
133
|
+
init.body = new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } });
|
|
134
|
+
init.duplex = "half";
|
|
135
|
+
}
|
|
136
|
+
const response = await this.#fetch(capability.url, init);
|
|
137
|
+
void response.body?.cancel().catch(() => {});
|
|
138
|
+
if (!response.ok) throw new Error(`Object upload failed (${response.status}).`);
|
|
139
|
+
return this.completeUpload(capability.id, options);
|
|
140
|
+
}
|
|
141
|
+
/** Fetches file bytes without sending the project token to the object store.
|
|
142
|
+
* @param {string} id @param {FileRequestOptions} [options] @returns {Promise<Blob>} */
|
|
143
|
+
async download(id, options = {}) {
|
|
144
|
+
const capability = await this.downloadUrl(id, options);
|
|
145
|
+
const response = await this.#fetch(capability.url, { method: "GET", credentials: "omit", redirect: "error", signal: options.signal ?? null });
|
|
146
|
+
if (!response.ok) throw new Error(`Object download failed (${response.status}).`);
|
|
147
|
+
if (response.body === null) throw new Error("Object download returned no body.");
|
|
148
|
+
const reader = response.body.getReader();
|
|
149
|
+
/** @type {Uint8Array<ArrayBuffer>[]} */ const chunks = [];
|
|
150
|
+
let length = 0;
|
|
151
|
+
try {
|
|
152
|
+
for (;;) {
|
|
153
|
+
options.signal?.throwIfAborted();
|
|
154
|
+
const part = await reader.read();
|
|
155
|
+
if (part.done) break;
|
|
156
|
+
length += part.value.byteLength;
|
|
157
|
+
if (length > maxFileBytes) throw new Error("Object download exceeds the file size limit.");
|
|
158
|
+
chunks.push(new Uint8Array(part.value));
|
|
159
|
+
}
|
|
160
|
+
} finally { await reader.cancel().catch(() => {}); reader.releaseLock(); }
|
|
161
|
+
return new Blob(chunks, { type: "application/octet-stream" });
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/v1/routes.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { fileStorageRoutes } from "./file-storage-routes.js";
|
|
1
2
|
import { managementV1Routes } from "./management-routes.js";
|
|
2
3
|
// Current TerraBase routes at e30ffdc; readShape is retained from release 6cb9745 only.
|
|
3
|
-
/** @typedef {import("./management-routes.js").ManagementOperation | "readFreeShape" | "getShapeKey" | "getDocument" | "putDocument" | "createDocument" | "patchDocument" | "deleteDocument" | "commitTransaction" | "queryDocuments" | "createCollection" | "getCollection" | "listCollections" | "dropCollection" | "readShape" | "runSql" | "checkSql" | "explainSql" | "streamDatabaseChanges" | "listDocuments" | "getCollectionSchema" | "putCollectionSchema" | "deleteCollectionSchema" | "createDatabase" | "listDatabases" | "getDatabase" | "updateDatabase" | "deleteDatabase" | "branchDatabase"} V1Operation */
|
|
4
|
+
/** @typedef {import("./file-storage-routes.js").FileStorageOperation | import("./management-routes.js").ManagementOperation | "readFreeShape" | "getShapeKey" | "getDocument" | "putDocument" | "createDocument" | "patchDocument" | "deleteDocument" | "commitTransaction" | "queryDocuments" | "createCollection" | "getCollection" | "listCollections" | "dropCollection" | "readShape" | "runSql" | "checkSql" | "explainSql" | "streamDatabaseChanges" | "listDocuments" | "getCollectionSchema" | "putCollectionSchema" | "deleteCollectionSchema" | "createDatabase" | "listDatabases" | "getDatabase" | "updateDatabase" | "deleteDatabase" | "branchDatabase"} V1Operation */
|
|
4
5
|
/**
|
|
5
6
|
* @typedef {object} V1Route
|
|
6
7
|
* @property {V1Operation} operation
|
|
@@ -61,7 +62,7 @@ export const v1Routes = Object.freeze(/** @type {V1Route[]} */ ([
|
|
|
61
62
|
})));
|
|
62
63
|
/** @param {V1Operation} operation @returns {V1Route} */
|
|
63
64
|
export function v1Route(operation) {
|
|
64
|
-
const route = [...v1Routes, ...shapeV1Routes, ...managementV1Routes].find(candidate => candidate.operation === operation);
|
|
65
|
+
const route = [...v1Routes, ...shapeV1Routes, ...managementV1Routes, ...fileStorageRoutes].find(candidate => candidate.operation === operation);
|
|
65
66
|
if (route === undefined) throw new TypeError(`Unknown v1 operation ${operation}.`);
|
|
66
67
|
return route;
|
|
67
68
|
}
|
package/src/v1/terrabase-v1.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { V1FileStorage } from "./file-storage.js";
|
|
1
2
|
// The TerraBase v1 client: documents, transactions, queries and shapes over
|
|
2
3
|
// the project-scoped C# Documents API (decision 0004 and shape-reads.md).
|
|
3
4
|
import {
|
|
@@ -1003,6 +1004,8 @@ export class V1Database {
|
|
|
1003
1004
|
* privately; it is never serialized, logged or forwarded to redirects.
|
|
1004
1005
|
*/
|
|
1005
1006
|
export class TerraBaseV1Client {
|
|
1007
|
+
/** @type {V1FileStorage} */
|
|
1008
|
+
#storage;
|
|
1006
1009
|
/** @type {V1Health} */
|
|
1007
1010
|
#health;
|
|
1008
1011
|
/** @type {V1Platform} */
|
|
@@ -1022,6 +1025,7 @@ export class TerraBaseV1Client {
|
|
|
1022
1025
|
constructor(config) {
|
|
1023
1026
|
requireId(config.project);
|
|
1024
1027
|
this.#transport = new V1Transport(config);
|
|
1028
|
+
this.#storage = new V1FileStorage(this.#transport, config.fetch ?? globalThis.fetch.bind(globalThis));
|
|
1025
1029
|
this.#databases = new V1Databases(this.#transport);
|
|
1026
1030
|
this.#savedQueries = new V1SavedQueries(this.#transport);
|
|
1027
1031
|
this.#metrics = new V1ProjectUsageClient(this.#transport);
|
|
@@ -1041,6 +1045,8 @@ export class TerraBaseV1Client {
|
|
|
1041
1045
|
|
|
1042
1046
|
/** @returns {V1Databases} Project-scoped database catalog. */
|
|
1043
1047
|
get databases() { return this.#databases; }
|
|
1048
|
+
/** @returns {V1FileStorage} Project file buckets. */
|
|
1049
|
+
get storage() { return this.#storage; }
|
|
1044
1050
|
/** @returns {V1SavedQueries} */
|
|
1045
1051
|
get savedQueries() { return this.#savedQueries; }
|
|
1046
1052
|
/** @returns {V1ProjectUsageClient} */
|
package/src/v1/usage.js
CHANGED
|
@@ -8,7 +8,7 @@ const period = record({ from: text, to: text, granularity: text, reads: numeric,
|
|
|
8
8
|
const series = list(record({ start: text, reads: numeric, writes: numeric }));
|
|
9
9
|
const databaseUsage = record({ databaseId: requireId, storageBytes: numeric, logicalBytes: numeric, collections: numeric,
|
|
10
10
|
documents: numeric, reads: numeric, writes: numeric, period, series });
|
|
11
|
-
const projectUsage = record({ projectId: requireId, databases: numeric, storageBytes: numeric, logicalBytes: numeric,
|
|
11
|
+
const projectUsage = record({ projectId: requireId, databases: numeric, storageBytes: numeric, logicalBytes: numeric, fileBytes: numeric,
|
|
12
12
|
documents: numeric, reads: numeric, writes: numeric, period, series, items: list(databaseUsage) });
|
|
13
13
|
const bandwidth = record({ bytesSent: numeric, bytesReceived: numeric });
|
|
14
14
|
const directedBandwidth = list(record({ sourceRegion: text, destinationRegion: text, kind: text, bytesSent: numeric, bytesReceived: numeric }));
|
package/types/index.d.ts
CHANGED
|
@@ -128,6 +128,14 @@ export type V1ShapeManifest = import("./v1/shape-manifest.js").V1ShapeManifest;
|
|
|
128
128
|
export type V1ShapeManifestRequest = import("./v1/shape-manifest.js").V1ShapeManifestRequest;
|
|
129
129
|
export type V1ShapeKeyRequest = import("./v1/shape-reader.js").V1ShapeKeyRequest;
|
|
130
130
|
export type V1ShapeKey = import("./v1/shape-reader.js").V1ShapeKey;
|
|
131
|
+
export type FileAccessPolicy = import("./v1/file-storage.js").FileAccessPolicy;
|
|
132
|
+
export type FileAccessRules = import("./v1/file-storage.js").FileAccessRules;
|
|
133
|
+
export type FileBucketInfo = import("./v1/file-storage.js").FileBucketInfo;
|
|
134
|
+
export type StoredFile = import("./v1/file-storage.js").StoredFile;
|
|
135
|
+
export type SignedFileUrl = import("./v1/file-storage.js").SignedFileUrl;
|
|
136
|
+
export type FileUploadRequest = import("./v1/file-storage.js").FileUploadRequest;
|
|
137
|
+
export type FileUploadOptions = import("./v1/file-storage.js").FileUploadOptions;
|
|
138
|
+
export type FileListOptions = import("./v1/file-storage.js").FileListOptions;
|
|
131
139
|
export { createTerraBaseV1Client as createTerraBaseClient, TerraBaseV1Client as TerraBaseClient, createTerraBaseV1Client, TerraBaseV1Client, V1Database, V1Collection, V1Collections, V1Transaction } from "./v1/terrabase-v1.js";
|
|
132
140
|
export { terraScaleReleaseBlockers, TerraScaleCapabilityUnavailableError } from "./management-contracts.js";
|
|
133
141
|
export { TerraBaseV1Error, V1PreconditionFailedError, V1InvalidKeyError, V1ForbiddenScopeError, V1NotFoundError, SqlError, V1ErrorCode, isTerraBaseV1Error } from "./v1/errors.js";
|
|
@@ -135,3 +143,4 @@ export { generateId, isValidId, requireId } from "./ids.js";
|
|
|
135
143
|
export { V1ProjectUsageClient, V1DatabaseUsageClient } from "./v1/usage.js";
|
|
136
144
|
export { V1Backups, V1BackupDestinationClient } from "./v1/backups.js";
|
|
137
145
|
export { createTerraBaseSystemClient, V1Health, V1Platform } from "./v1/system.js";
|
|
146
|
+
export { V1FileStorage, V1FileBucket } from "./v1/file-storage.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** @type {readonly import("./routes.js").V1Route[]} */
|
|
2
|
+
export const fileStorageRoutes: readonly import("./routes.js").V1Route[];
|
|
3
|
+
export type FileStorageOperation = "createFileBucket" | "listFileBuckets" | "setFileBucketRules" | "deleteFileBucket" | "createFileUpload" | "completeFileUpload" | "listBucketFiles" | "createFileDownload" | "deleteBucketFile";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** Project file buckets, authenticated by the same token as documents. */
|
|
2
|
+
export class V1FileStorage {
|
|
3
|
+
/** @param {V1Transport} transport @param {FetchLike} fetcher */
|
|
4
|
+
constructor(transport: V1Transport, fetcher: FetchLike);
|
|
5
|
+
/** @param {string} name @param {FileAccessRules} accessRules @param {FileRequestOptions} [options] @returns {Promise<FileBucketInfo>} */
|
|
6
|
+
createBucket(name: string, accessRules: FileAccessRules, options?: FileRequestOptions): Promise<FileBucketInfo>;
|
|
7
|
+
/** @param {FileListOptions} [options] @returns {Promise<{items: FileBucketInfo[], next: string | null}>} */
|
|
8
|
+
listBuckets(options?: FileListOptions): Promise<{
|
|
9
|
+
items: FileBucketInfo[];
|
|
10
|
+
next: string | null;
|
|
11
|
+
}>;
|
|
12
|
+
/** @param {string} id @returns {V1FileBucket} */
|
|
13
|
+
bucket(id: string): V1FileBucket;
|
|
14
|
+
#private;
|
|
15
|
+
}
|
|
16
|
+
/** Immutable files in one project bucket. */
|
|
17
|
+
export class V1FileBucket {
|
|
18
|
+
/** @param {V1Transport} transport @param {FetchLike} fetcher @param {string} bucket */
|
|
19
|
+
constructor(transport: V1Transport, fetcher: FetchLike, bucket: string);
|
|
20
|
+
/** @param {FileAccessRules} accessRules @param {FileRequestOptions} [options] @returns {Promise<FileBucketInfo>} */
|
|
21
|
+
setRules(accessRules: FileAccessRules, options?: FileRequestOptions): Promise<FileBucketInfo>;
|
|
22
|
+
/** @param {FileRequestOptions} [options] @returns {Promise<void>} */
|
|
23
|
+
delete(options?: FileRequestOptions): Promise<void>;
|
|
24
|
+
/** @param {FileUploadRequest} request @param {FileRequestOptions} [options] @returns {Promise<SignedFileUrl>} */
|
|
25
|
+
createUpload(request: FileUploadRequest, options?: FileRequestOptions): Promise<SignedFileUrl>;
|
|
26
|
+
/** @param {string} id @param {FileRequestOptions} [options] @returns {Promise<StoredFile>} */
|
|
27
|
+
completeUpload(id: string, options?: FileRequestOptions): Promise<StoredFile>;
|
|
28
|
+
/** @param {FileListOptions} [options] @returns {Promise<{items: StoredFile[], next: string | null}>} */
|
|
29
|
+
list(options?: FileListOptions): Promise<{
|
|
30
|
+
items: StoredFile[];
|
|
31
|
+
next: string | null;
|
|
32
|
+
}>;
|
|
33
|
+
/** @param {string} id @param {FileRequestOptions} [options] @returns {Promise<SignedFileUrl>} */
|
|
34
|
+
downloadUrl(id: string, options?: FileRequestOptions): Promise<SignedFileUrl>;
|
|
35
|
+
/** @param {string} id @param {FileRequestOptions} [options] @returns {Promise<void>} */
|
|
36
|
+
deleteFile(id: string, options?: FileRequestOptions): Promise<void>;
|
|
37
|
+
/** Hashes a private byte snapshot, uploads it without project credentials, and verifies completion. No retries.
|
|
38
|
+
* @param {string} name @param {Uint8Array | Blob} input @param {FileUploadOptions} options @returns {Promise<StoredFile>} */
|
|
39
|
+
upload(name: string, input: Uint8Array | Blob, options: FileUploadOptions): Promise<StoredFile>;
|
|
40
|
+
/** Fetches file bytes without sending the project token to the object store.
|
|
41
|
+
* @param {string} id @param {FileRequestOptions} [options] @returns {Promise<Blob>} */
|
|
42
|
+
download(id: string, options?: FileRequestOptions): Promise<Blob>;
|
|
43
|
+
#private;
|
|
44
|
+
}
|
|
45
|
+
export type FileAccessPolicy = {
|
|
46
|
+
owner: boolean;
|
|
47
|
+
public: boolean;
|
|
48
|
+
roles: readonly string[];
|
|
49
|
+
};
|
|
50
|
+
export type FileAccessRules = {
|
|
51
|
+
owner: string | null;
|
|
52
|
+
read: FileAccessPolicy;
|
|
53
|
+
write: FileAccessPolicy;
|
|
54
|
+
};
|
|
55
|
+
export type FileBucketInfo = {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
region: string;
|
|
59
|
+
rules: FileAccessRules;
|
|
60
|
+
};
|
|
61
|
+
export type StoredFile = {
|
|
62
|
+
id: string;
|
|
63
|
+
name: string;
|
|
64
|
+
contentType: string;
|
|
65
|
+
size: number;
|
|
66
|
+
sha256: string;
|
|
67
|
+
metadata: JsonObject;
|
|
68
|
+
status: string;
|
|
69
|
+
uploadExpiresAt: string;
|
|
70
|
+
};
|
|
71
|
+
export type SignedFileUrl = {
|
|
72
|
+
id: string;
|
|
73
|
+
url: string;
|
|
74
|
+
expiresAt: string;
|
|
75
|
+
headers: Readonly<Record<string, string>>;
|
|
76
|
+
};
|
|
77
|
+
export type FileUploadRequest = {
|
|
78
|
+
name: string;
|
|
79
|
+
contentType: string;
|
|
80
|
+
size: number;
|
|
81
|
+
sha256: string;
|
|
82
|
+
metadata: JsonObject;
|
|
83
|
+
};
|
|
84
|
+
export type FileRequestOptions = {
|
|
85
|
+
signal?: AbortSignal | undefined;
|
|
86
|
+
};
|
|
87
|
+
export type FileListOptions = {
|
|
88
|
+
after?: string | undefined;
|
|
89
|
+
signal?: AbortSignal | undefined;
|
|
90
|
+
};
|
|
91
|
+
export type FileUploadOptions = {
|
|
92
|
+
metadata: JsonObject;
|
|
93
|
+
contentType?: string | undefined;
|
|
94
|
+
signal?: AbortSignal | undefined;
|
|
95
|
+
};
|
|
96
|
+
import type { V1Transport } from "./transport.js";
|
|
97
|
+
import type { FetchLike } from "../config.js";
|
|
98
|
+
import type { JsonObject } from "./json.js";
|
package/types/v1/routes.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export function v1RoutePath(route: V1Route, params: Readonly<Record<string, stri
|
|
|
6
6
|
export const v1Routes: readonly V1Route[];
|
|
7
7
|
/** @type {readonly V1Route[]} */
|
|
8
8
|
export const shapeV1Routes: readonly V1Route[];
|
|
9
|
-
export type V1Operation = import("./management-routes.js").ManagementOperation | "readFreeShape" | "getShapeKey" | "getDocument" | "putDocument" | "createDocument" | "patchDocument" | "deleteDocument" | "commitTransaction" | "queryDocuments" | "createCollection" | "getCollection" | "listCollections" | "dropCollection" | "readShape" | "runSql" | "checkSql" | "explainSql" | "streamDatabaseChanges" | "listDocuments" | "getCollectionSchema" | "putCollectionSchema" | "deleteCollectionSchema" | "createDatabase" | "listDatabases" | "getDatabase" | "updateDatabase" | "deleteDatabase" | "branchDatabase";
|
|
9
|
+
export type V1Operation = import("./file-storage-routes.js").FileStorageOperation | import("./management-routes.js").ManagementOperation | "readFreeShape" | "getShapeKey" | "getDocument" | "putDocument" | "createDocument" | "patchDocument" | "deleteDocument" | "commitTransaction" | "queryDocuments" | "createCollection" | "getCollection" | "listCollections" | "dropCollection" | "readShape" | "runSql" | "checkSql" | "explainSql" | "streamDatabaseChanges" | "listDocuments" | "getCollectionSchema" | "putCollectionSchema" | "deleteCollectionSchema" | "createDatabase" | "listDatabases" | "getDatabase" | "updateDatabase" | "deleteDatabase" | "branchDatabase";
|
|
10
10
|
export type V1Route = {
|
|
11
11
|
operation: V1Operation;
|
|
12
12
|
method: "GET" | "PUT" | "POST" | "PATCH" | "DELETE";
|
|
@@ -341,6 +341,8 @@ export class TerraBaseV1Client {
|
|
|
341
341
|
database(name: string): V1Database;
|
|
342
342
|
/** @returns {V1Databases} Project-scoped database catalog. */
|
|
343
343
|
get databases(): V1Databases;
|
|
344
|
+
/** @returns {V1FileStorage} Project file buckets. */
|
|
345
|
+
get storage(): V1FileStorage;
|
|
344
346
|
/** @returns {V1SavedQueries} */
|
|
345
347
|
get savedQueries(): V1SavedQueries;
|
|
346
348
|
/** @returns {V1ProjectUsageClient} */
|
|
@@ -592,6 +594,7 @@ import { V1DatabaseUsageClient } from "./usage.js";
|
|
|
592
594
|
import { V1Indexes } from "./indexes.js";
|
|
593
595
|
import { V1DatabaseSchemaClient } from "./database-schema.js";
|
|
594
596
|
import { V1Databases } from "./catalog.js";
|
|
597
|
+
import { V1FileStorage } from "./file-storage.js";
|
|
595
598
|
import { V1SavedQueries } from "./saved-queries.js";
|
|
596
599
|
import { V1ProjectUsageClient } from "./usage.js";
|
|
597
600
|
import { V1BackupDestinationClient } from "./backups.js";
|
package/types/v1/usage.d.ts
CHANGED
|
@@ -63,6 +63,7 @@ declare const projectUsage: import("./management-decode.js").Decoder<Readonly<{
|
|
|
63
63
|
databases: number | import("./json.js").ExactDecimal;
|
|
64
64
|
storageBytes: number | import("./json.js").ExactDecimal;
|
|
65
65
|
logicalBytes: number | import("./json.js").ExactDecimal;
|
|
66
|
+
fileBytes: number | import("./json.js").ExactDecimal;
|
|
66
67
|
documents: number | import("./json.js").ExactDecimal;
|
|
67
68
|
reads: number | import("./json.js").ExactDecimal;
|
|
68
69
|
writes: number | import("./json.js").ExactDecimal;
|