@editframe/api 0.7.0-beta.8 → 0.8.0-beta.10

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 (59) hide show
  1. package/dist/CHUNK_SIZE_BYTES.d.ts +1 -0
  2. package/dist/CHUNK_SIZE_BYTES.js +7 -0
  3. package/dist/client.d.ts +6 -0
  4. package/dist/client.js +13 -7
  5. package/dist/client.test.d.ts +1 -0
  6. package/dist/index.d.ts +8 -0
  7. package/dist/index.js +11 -1
  8. package/dist/readableFromBuffers.d.ts +2 -0
  9. package/dist/resources/caption-file.d.ts +39 -0
  10. package/dist/resources/caption-file.js +28 -26
  11. package/dist/resources/caption-file.test.d.ts +1 -0
  12. package/dist/resources/image-file.d.ts +31 -0
  13. package/dist/resources/image-file.js +23 -27
  14. package/dist/resources/image-file.test.d.ts +1 -0
  15. package/dist/resources/isobmff-file.d.ts +19 -0
  16. package/dist/resources/isobmff-file.js +17 -23
  17. package/dist/resources/isobmff-file.test.d.ts +1 -0
  18. package/dist/resources/isobmff-track.d.ts +270 -0
  19. package/dist/resources/isobmff-track.js +18 -31
  20. package/dist/resources/isobmff-track.test.d.ts +1 -0
  21. package/dist/resources/renders.d.ts +34 -0
  22. package/dist/resources/renders.js +17 -21
  23. package/dist/resources/renders.test.d.ts +1 -0
  24. package/dist/resources/unprocessed-file.d.ts +45 -0
  25. package/dist/resources/unprocessed-file.js +133 -0
  26. package/dist/resources/unprocessed-file.test.d.ts +1 -0
  27. package/dist/resources/url-token.d.ts +5 -0
  28. package/dist/resources/url-token.js +20 -0
  29. package/dist/resources/url-token.test.d.ts +1 -0
  30. package/dist/streamChunker.d.ts +2 -0
  31. package/dist/streamChunker.js +17 -0
  32. package/dist/streamChunker.test.d.ts +1 -0
  33. package/dist/uploadChunks.d.ts +10 -0
  34. package/dist/uploadChunks.js +65 -0
  35. package/dist/uploadChunks.test.d.ts +1 -0
  36. package/package.json +11 -11
  37. package/src/resources/caption-file.test.ts +124 -0
  38. package/src/resources/caption-file.ts +50 -25
  39. package/src/resources/image-file.test.ts +138 -0
  40. package/src/resources/image-file.ts +29 -26
  41. package/src/resources/isobmff-file.test.ts +108 -0
  42. package/src/resources/isobmff-file.ts +20 -23
  43. package/src/resources/isobmff-track.test.ts +152 -0
  44. package/src/resources/isobmff-track.ts +23 -32
  45. package/src/resources/renders.test.ts +112 -0
  46. package/src/resources/renders.ts +20 -21
  47. package/src/resources/test-av-file.txt +1 -0
  48. package/src/resources/unprocessed-file.test.ts +312 -0
  49. package/src/resources/unprocessed-file.ts +189 -0
  50. package/src/resources/url-token.test.ts +46 -0
  51. package/src/resources/url-token.ts +27 -0
  52. package/dist/client.cjs +0 -29
  53. package/dist/index.cjs +0 -24
  54. package/dist/resources/caption-file.cjs +0 -56
  55. package/dist/resources/image-file.cjs +0 -52
  56. package/dist/resources/isobmff-file.cjs +0 -56
  57. package/dist/resources/isobmff-track.cjs +0 -71
  58. package/dist/resources/renders.cjs +0 -56
  59. package/src/util/nodeStreamToWebStream.ts +0 -20
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { Readable } from 'node:stream';
2
+ import { Client } from './client.ts';
3
+ interface UploadChunksOptions {
4
+ url: string;
5
+ fileStream: Readable;
6
+ fileSize: number;
7
+ chunkSizeBytes?: number;
8
+ }
9
+ export declare function uploadChunks(client: Client, { url, fileSize, fileStream, chunkSizeBytes, }: UploadChunksOptions): Promise<void>;
10
+ export {};
@@ -0,0 +1,65 @@
1
+ import debug from "debug";
2
+ import { streamChunker } from "./streamChunker.js";
3
+ import { CHUNK_SIZE_BYTES } from "./CHUNK_SIZE_BYTES.js";
4
+ const log = debug("ef:api:uploadChunk");
5
+ const uploadChunk = async (client, {
6
+ url,
7
+ chunkBuffer,
8
+ chunkNumber,
9
+ fileSize,
10
+ chunkSizeBytes = CHUNK_SIZE_BYTES
11
+ }) => {
12
+ const startByte = chunkNumber * chunkSizeBytes;
13
+ const endByte = startByte + chunkBuffer.length - 1;
14
+ log(`Uploading chunk ${chunkNumber} for ${url}`);
15
+ const response = await client.authenticatedFetch(url, {
16
+ method: "POST",
17
+ headers: {
18
+ "Content-Range": `bytes=${startByte}-${endByte}/${fileSize}`,
19
+ "Content-Type": "application/octet-stream"
20
+ },
21
+ body: chunkBuffer
22
+ });
23
+ if (response.ok) {
24
+ if (response.status === 201) {
25
+ log(`File ${url} fully uploaded`);
26
+ return { complete: true, body: await response.json() };
27
+ }
28
+ if (response.status === 202) {
29
+ log(`File ${url} chunk ${chunkNumber} uploaded`);
30
+ return { complete: false, body: await response.json() };
31
+ }
32
+ }
33
+ throw new Error(
34
+ `Failed to upload chunk ${chunkNumber} for ${url} ${response.status} ${response.statusText}`
35
+ );
36
+ };
37
+ async function uploadChunks(client, {
38
+ url,
39
+ fileSize,
40
+ fileStream,
41
+ chunkSizeBytes = CHUNK_SIZE_BYTES
42
+ }) {
43
+ let chunkNumber = 0;
44
+ let complete = false;
45
+ for await (const chunkBuffer of streamChunker(fileStream, chunkSizeBytes)) {
46
+ log(`Uploading chunk ${chunkNumber}`);
47
+ ({ complete } = await uploadChunk(client, {
48
+ url,
49
+ chunkBuffer,
50
+ chunkNumber,
51
+ fileSize,
52
+ chunkSizeBytes
53
+ }));
54
+ chunkNumber++;
55
+ }
56
+ if (!fileStream.readableEnded) {
57
+ throw new Error("Did not read entire file stream");
58
+ }
59
+ if (!complete) {
60
+ throw new Error("Did not complete upload");
61
+ }
62
+ }
63
+ export {
64
+ uploadChunks
65
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,16 +1,12 @@
1
1
  {
2
2
  "name": "@editframe/api",
3
- "version": "0.7.0-beta.8",
3
+ "version": "0.8.0-beta.10",
4
4
  "description": "API functions for EditFrame",
5
5
  "exports": {
6
6
  ".": {
7
7
  "import": {
8
- "default": "./dist/index.js",
9
- "types": "./dist/index.d.ts"
10
- },
11
- "require": {
12
- "default": "./dist/index.cjs",
13
- "types": "./dist/index.d.ts"
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
14
10
  }
15
11
  }
16
12
  },
@@ -23,15 +19,19 @@
23
19
  "author": "",
24
20
  "license": "UNLICENSED",
25
21
  "devDependencies": {
26
- "@types/node": "^20.14.9",
27
- "typescript": "^5.2.2",
22
+ "@types/jsonwebtoken": "^9.0.6",
23
+ "@types/node": "^20.14.13",
24
+ "typedoc": "^0.26.5",
25
+ "typescript": "^5.5.4",
28
26
  "vite": "^5.2.11",
29
- "vite-plugin-dts": "^3.9.1",
27
+ "vite-plugin-dts": "^4.0.3",
30
28
  "vite-tsconfig-paths": "^4.3.2"
31
29
  },
32
30
  "dependencies": {
33
- "@editframe/assets": "0.7.0-beta.8",
31
+ "@editframe/assets": "0.8.0-beta.10",
34
32
  "debug": "^4.3.5",
33
+ "jsonwebtoken": "^9.0.2",
34
+ "node-fetch": "^3.3.2",
35
35
  "zod": "^3.23.8"
36
36
  }
37
37
  }
@@ -0,0 +1,124 @@
1
+ import { test, expect, beforeAll, afterEach, afterAll, describe } from "vitest";
2
+ import { http, HttpResponse } from "msw";
3
+ import { setupServer } from "msw/node";
4
+
5
+ import { Client } from "../client.ts";
6
+ import { createCaptionFile, uploadCaptionFile } from "./caption-file.ts";
7
+ import { readableFromBuffers } from "../readableFromBuffers.ts";
8
+
9
+ const server = setupServer();
10
+ const client = new Client("ef_TEST_TOKEN", "http://localhost");
11
+
12
+ describe("CaptionFile", () => {
13
+ beforeAll(() => server.listen());
14
+ afterEach(() => server.resetHandlers());
15
+ afterAll(() => server.close());
16
+
17
+ describe("createCaptionFile", () => {
18
+ test("Throws when file is too large", async () => {
19
+ await expect(
20
+ createCaptionFile(client, {
21
+ id: "test-id",
22
+ filename: "test",
23
+ byte_size: 1024 * 1024 * 3,
24
+ }),
25
+ ).rejects.toThrowError(
26
+ "File size 3145728 bytes exceeds limit 2097152 bytes",
27
+ );
28
+ });
29
+
30
+ test("Throws when server returns an error", async () => {
31
+ server.use(
32
+ http.post("http://localhost/api/video2/caption_files", () =>
33
+ HttpResponse.text("Internal Server Error", { status: 500 }),
34
+ ),
35
+ );
36
+
37
+ await expect(
38
+ createCaptionFile(client, {
39
+ id: "test-id",
40
+ filename: "test",
41
+ byte_size: 4,
42
+ }),
43
+ ).rejects.toThrowError(
44
+ "Failed to create caption 500 Internal Server Error",
45
+ );
46
+ });
47
+
48
+ test("Returns json data from the http response", async () => {
49
+ server.use(
50
+ http.post("http://localhost/api/video2/caption_files", () =>
51
+ HttpResponse.json(
52
+ { id: "test-id" },
53
+ { status: 200, statusText: "OK" },
54
+ ),
55
+ ),
56
+ );
57
+
58
+ const response = await createCaptionFile(client, {
59
+ id: "test-id",
60
+ filename: "test",
61
+ byte_size: 4,
62
+ });
63
+
64
+ expect(response).toEqual({ id: "test-id" });
65
+ });
66
+ });
67
+
68
+ describe("uploadCaptionFile", () => {
69
+ test("Throws when file is too large", async () => {
70
+ await expect(
71
+ uploadCaptionFile(
72
+ client,
73
+ "test-id",
74
+ readableFromBuffers(Buffer.from("test")),
75
+ 1024 * 1024 * 3,
76
+ ),
77
+ ).rejects.toThrowError(
78
+ "File size 3145728 bytes exceeds limit 2097152 bytes",
79
+ );
80
+ });
81
+
82
+ test("Throws when server returns an error", async () => {
83
+ server.use(
84
+ http.post(
85
+ "http://localhost/api/video2/caption_files/test-id/upload",
86
+ () => HttpResponse.text("Internal Server Error", { status: 500 }),
87
+ ),
88
+ );
89
+
90
+ await expect(
91
+ uploadCaptionFile(
92
+ client,
93
+ "test-id",
94
+ readableFromBuffers(Buffer.from("nice")),
95
+ 4,
96
+ ),
97
+ ).rejects.toThrowError(
98
+ "Failed to upload caption 500 Internal Server Error",
99
+ );
100
+ });
101
+
102
+ test("Returns json data from the http response", async () => {
103
+ server.use(
104
+ http.post(
105
+ "http://localhost/api/video2/caption_files/test-id/upload",
106
+ () =>
107
+ HttpResponse.json(
108
+ { id: "test-id" },
109
+ { status: 200, statusText: "OK" },
110
+ ),
111
+ ),
112
+ );
113
+
114
+ const response = await uploadCaptionFile(
115
+ client,
116
+ "test-id",
117
+ readableFromBuffers(Buffer.from("nice")),
118
+ 4,
119
+ );
120
+
121
+ expect(response).toEqual({ id: "test-id" });
122
+ });
123
+ });
124
+ });
@@ -3,13 +3,16 @@ import type { Readable } from "node:stream";
3
3
  import { z } from "zod";
4
4
  import debug from "debug";
5
5
 
6
- import type { Client } from "../client";
6
+ import type { Client } from "../client.ts";
7
7
 
8
8
  const log = debug("ef:api:caption-file");
9
9
 
10
+ const MAX_CAPTION_SIZE = 1024 * 1024 * 2; // 2MB
11
+
10
12
  export const CreateCaptionFilePayload = z.object({
11
13
  id: z.string(),
12
14
  filename: z.string(),
15
+ byte_size: z.number().int().max(MAX_CAPTION_SIZE),
13
16
  });
14
17
 
15
18
  export interface CreateCaptionFileResult {
@@ -17,56 +20,78 @@ export interface CreateCaptionFileResult {
17
20
  id: string;
18
21
  }
19
22
 
23
+ const restrictSize = (size: number) => {
24
+ if (size > MAX_CAPTION_SIZE) {
25
+ throw new Error(
26
+ `File size ${size} bytes exceeds limit ${MAX_CAPTION_SIZE} bytes\n`,
27
+ );
28
+ }
29
+ };
30
+
31
+ /**
32
+ * Create a caption file
33
+ * @param client - The authenticated client to use for the request
34
+ * @param payload - The payload to send to the server
35
+ * @returns The result of the request
36
+ * @example
37
+ * ```ts
38
+ * const result = await createCaptionFile(client, {
39
+ * id: "123",
40
+ * filename: "caption.srt",
41
+ * });
42
+ * console.log(result);
43
+ * ```
44
+ * @category CaptionFile
45
+ * @resource
46
+ * @beta
47
+ */
20
48
  export const createCaptionFile = async (
21
49
  client: Client,
22
50
  payload: z.infer<typeof CreateCaptionFilePayload>,
23
51
  ) => {
24
52
  log("Creating caption file", payload);
25
- const fileCreation = await client.authenticatedFetch(
53
+ restrictSize(payload.byte_size);
54
+ const response = await client.authenticatedFetch(
26
55
  "/api/video2/caption_files",
27
56
  {
28
57
  method: "POST",
29
58
  body: JSON.stringify(payload),
30
59
  },
31
60
  );
32
- log("Caption file created", fileCreation);
61
+ log("Caption file created", response);
33
62
 
34
- switch (fileCreation.status) {
35
- case 200: {
36
- return (await fileCreation.json()) as CreateCaptionFileResult;
37
- }
38
- default: {
39
- console.error(
40
- `Failed to create file ${fileCreation.status} ${fileCreation.statusText}`,
41
- );
42
- return;
43
- }
63
+ if (response.ok) {
64
+ return (await response.json()) as CreateCaptionFileResult;
44
65
  }
66
+
67
+ throw new Error(
68
+ `Failed to create caption ${response.status} ${response.statusText}`,
69
+ );
45
70
  };
46
71
 
47
72
  export const uploadCaptionFile = async (
48
73
  client: Client,
49
74
  fileId: string,
50
75
  fileStream: Readable,
76
+ fileSize: number,
51
77
  ) => {
52
78
  log("Uploading caption file", fileId);
53
- const fileIndex = await client.authenticatedFetch(
79
+ restrictSize(fileSize);
80
+
81
+ const response = await client.authenticatedFetch(
54
82
  `/api/video2/caption_files/${fileId}/upload`,
55
83
  {
56
84
  method: "POST",
57
85
  body: fileStream,
58
86
  },
59
87
  );
60
- log("Caption file uploaded", fileIndex);
61
- switch (fileIndex.status) {
62
- case 200: {
63
- return fileIndex.json();
64
- }
65
- default: {
66
- console.error(
67
- `Failed to upload caption ${fileIndex.status} ${fileIndex.statusText}`,
68
- );
69
- return;
70
- }
88
+ log("Caption file uploaded", response);
89
+
90
+ if (response.ok) {
91
+ return response.json();
71
92
  }
93
+
94
+ throw new Error(
95
+ `Failed to upload caption ${response.status} ${response.statusText}`,
96
+ );
72
97
  };
@@ -0,0 +1,138 @@
1
+ import { test, expect, beforeAll, afterEach, afterAll, describe } from "vitest";
2
+ import { http, HttpResponse } from "msw";
3
+ import { setupServer } from "msw/node";
4
+ import { ZodError } from "zod";
5
+
6
+ import { Client } from "../client.ts";
7
+ import { createImageFile, uploadImageFile } from "./image-file.ts";
8
+ import { readableFromBuffers } from "../readableFromBuffers.ts";
9
+
10
+ const server = setupServer();
11
+ const client = new Client("ef_TEST_TOKEN", "http://localhost");
12
+
13
+ describe("ImageFile", () => {
14
+ beforeAll(() => server.listen());
15
+ afterEach(() => server.resetHandlers());
16
+ afterAll(() => server.close());
17
+
18
+ describe("createImageFile", () => {
19
+ test("Throws when file is too large", async () => {
20
+ await expect(
21
+ createImageFile(client, {
22
+ id: "test-id",
23
+ filename: "test",
24
+ byte_size: 1024 * 1024 * 17,
25
+ height: 100,
26
+ width: 100,
27
+ mime_type: "image/jpeg",
28
+ }),
29
+ ).rejects.toThrowError(
30
+ new ZodError([
31
+ {
32
+ code: "too_big",
33
+ maximum: 16777216,
34
+ type: "number",
35
+ inclusive: true,
36
+ exact: false,
37
+ message: "Number must be less than or equal to 16777216",
38
+ path: ["byte_size"],
39
+ },
40
+ ]),
41
+ );
42
+ });
43
+
44
+ test("Throws when server returns an error", async () => {
45
+ server.use(
46
+ http.post("http://localhost/api/video2/image_files", () =>
47
+ HttpResponse.text("Internal Server Error", { status: 500 }),
48
+ ),
49
+ );
50
+
51
+ await expect(
52
+ createImageFile(client, {
53
+ id: "test-id",
54
+ filename: "test",
55
+ byte_size: 4,
56
+ height: 100,
57
+ width: 100,
58
+ mime_type: "image/jpeg",
59
+ }),
60
+ ).rejects.toThrowError("Failed to create file 500 Internal Server Error");
61
+ });
62
+
63
+ test("Returns json data from the http response", async () => {
64
+ server.use(
65
+ http.post("http://localhost/api/video2/image_files", () =>
66
+ HttpResponse.json(
67
+ { id: "test-id" },
68
+ { status: 200, statusText: "OK" },
69
+ ),
70
+ ),
71
+ );
72
+
73
+ const response = await createImageFile(client, {
74
+ id: "test-id",
75
+ filename: "test",
76
+ byte_size: 4,
77
+ height: 100,
78
+ width: 100,
79
+ mime_type: "image/jpeg",
80
+ });
81
+
82
+ expect(response).toEqual({ id: "test-id" });
83
+ });
84
+ });
85
+
86
+ describe("uploadImageFile", () => {
87
+ test("Throws when file is too large", async () => {
88
+ await expect(
89
+ uploadImageFile(
90
+ client,
91
+ "test-file-id",
92
+ readableFromBuffers(Buffer.from("test")),
93
+ 1024 * 1024 * 17,
94
+ ),
95
+ ).rejects.toThrowError(
96
+ "File size 17825792 bytes exceeds limit 16777216 bytes",
97
+ );
98
+ });
99
+
100
+ test("Throws if upload fails", async () => {
101
+ server.use(
102
+ http.post(
103
+ "http://localhost/api/video2/image_files/test-file-id/upload",
104
+ () => HttpResponse.text("Internal Server Error", { status: 500 }),
105
+ ),
106
+ );
107
+
108
+ await expect(
109
+ uploadImageFile(
110
+ client,
111
+ "test-file-id",
112
+ readableFromBuffers(Buffer.from("test")),
113
+ 4,
114
+ ),
115
+ ).rejects.toThrowError(
116
+ "Failed to upload chunk 0 for /api/video2/image_files/test-file-id/upload 500 Internal Server Error",
117
+ );
118
+ });
119
+
120
+ test("Uploads file", async () => {
121
+ server.use(
122
+ http.post(
123
+ "http://localhost/api/video2/image_files/test-file-id/upload",
124
+ () => HttpResponse.json(null, { status: 201 }),
125
+ ),
126
+ );
127
+
128
+ await expect(
129
+ uploadImageFile(
130
+ client,
131
+ "test-file-id",
132
+ readableFromBuffers(Buffer.from("test")),
133
+ 4,
134
+ ),
135
+ ).resolves.toBeUndefined();
136
+ });
137
+ });
138
+ });
@@ -3,16 +3,20 @@ import type { Readable } from "node:stream";
3
3
  import { z } from "zod";
4
4
  import debug from "debug";
5
5
 
6
- import type { Client } from "../client";
6
+ import type { Client } from "../client.ts";
7
+ import { uploadChunks } from "../uploadChunks.ts";
7
8
 
8
9
  const log = debug("ef:api:image-file");
9
10
 
11
+ const MAX_IMAGE_SIZE = 1024 * 1024 * 16; // 16MB
12
+
10
13
  export const CreateImageFilePayload = z.object({
11
14
  id: z.string(),
12
15
  height: z.number().int(),
13
16
  width: z.number().int(),
14
17
  mime_type: z.enum(["image/jpeg", "image/png", "image/jpg", "image/webp"]),
15
18
  filename: z.string(),
19
+ byte_size: z.number().int().max(MAX_IMAGE_SIZE),
16
20
  });
17
21
 
18
22
  export interface CreateImageFileResult {
@@ -25,44 +29,43 @@ export const createImageFile = async (
25
29
  payload: z.infer<typeof CreateImageFilePayload>,
26
30
  ) => {
27
31
  log("Creating image file", payload);
32
+ CreateImageFilePayload.parse(payload);
28
33
  const response = await client.authenticatedFetch("/api/video2/image_files", {
29
34
  method: "POST",
30
35
  body: JSON.stringify(payload),
31
36
  });
32
37
 
33
38
  log("Image file created", response);
34
- switch (response.status) {
35
- case 200: {
36
- return (await response.json()) as CreateImageFileResult;
37
- }
38
- default: {
39
- console.error(
40
- `Failed to create file ${response.status} ${response.statusText}`,
41
- );
42
- return;
43
- }
39
+
40
+ if (response.ok) {
41
+ return (await response.json()) as CreateImageFileResult;
44
42
  }
43
+
44
+ throw new Error(
45
+ `Failed to create file ${response.status} ${response.statusText}`,
46
+ );
45
47
  };
46
48
 
47
49
  export const uploadImageFile = async (
48
50
  client: Client,
49
51
  fileId: string,
50
52
  fileStream: Readable,
53
+ fileSize: number,
51
54
  ) => {
52
- const fileIndex = await client.authenticatedFetch(
53
- `/api/video2/image_files/${fileId}/upload`,
54
- {
55
- method: "POST",
56
- body: fileStream,
57
- },
58
- );
59
- switch (fileIndex.status) {
60
- case 200: {
61
- return fileIndex.json();
62
- }
63
- default: {
64
- console.error("Failed to upload image");
65
- return;
66
- }
55
+ log("Uploading image file", fileId);
56
+ if (fileSize > MAX_IMAGE_SIZE) {
57
+ throw new Error(
58
+ `File size ${fileSize} bytes exceeds limit ${MAX_IMAGE_SIZE} bytes`,
59
+ );
67
60
  }
61
+
62
+ const result = await uploadChunks(client, {
63
+ url: `/api/video2/image_files/${fileId}/upload`,
64
+ fileSize,
65
+ fileStream,
66
+ });
67
+
68
+ log("Image file upload complete");
69
+
70
+ return result;
68
71
  };