talizen 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -23,6 +23,9 @@ import { setTalizenConfig } from "talizen/core"
23
23
 
24
24
  setTalizenConfig({
25
25
  baseUrl: "https://www.talizen.com",
26
+ onFileUploadProcess(key, process) {
27
+ console.log(key, process)
28
+ },
26
29
  })
27
30
  ```
28
31
 
@@ -74,6 +77,13 @@ await submitForm("contact-form", {
74
77
  })
75
78
  ```
76
79
 
80
+ When a `File` object appears in the payload, `submitForm()` will:
81
+
82
+ 1. Call `POST /form/:key/file/preupload`
83
+ 2. If `hash_exist` is `false`, upload the file to the returned S3 signed URL
84
+ 3. Replace the original `File` value with the returned `file_url`
85
+ 4. Submit the final payload to `/form/:key/submit`
86
+
77
87
  ## Package Layout
78
88
 
79
89
  - `talizen/core`: shared runtime config, request helpers, and base data types.
package/core.d.ts CHANGED
@@ -11,10 +11,12 @@ export interface ImgItem {
11
11
  width?: number;
12
12
  }
13
13
  export type Image = ImgItem[] | ImgItem;
14
+ export type TalizenFileUploadProcessCallback = (key: string, process: number) => void;
14
15
  export interface TalizenClientConfig {
15
16
  baseUrl?: string;
16
17
  headers?: HeadersInit;
17
18
  fetch?: typeof fetch;
19
+ onFileUploadProcess?: TalizenFileUploadProcessCallback;
18
20
  }
19
21
  export interface TalizenRequestOptions extends TalizenClientConfig {
20
22
  signal?: AbortSignal;
package/form.js CHANGED
@@ -1,9 +1,158 @@
1
- import { requestJson } from "./core.js";
1
+ import { requestJson, resolveTalizenConfig } from "./core.js";
2
2
  export async function submitForm(keyOrToken, payload, options) {
3
- return requestJson(`/form/${keyOrToken}/submit`, {
3
+ const formKey = getFormKey(keyOrToken);
4
+ const data = await replaceFiles(formKey, payload, "", options);
5
+ return requestJson(`/form/${formKey}/submit`, {
4
6
  method: "POST",
5
7
  body: JSON.stringify({
6
- data: payload,
8
+ data,
7
9
  }),
8
10
  }, options);
9
11
  }
12
+ function getFormKey(keyOrToken) {
13
+ if (keyOrToken == null || keyOrToken === "") {
14
+ throw new Error("Talizen form key is required.");
15
+ }
16
+ return keyOrToken;
17
+ }
18
+ async function replaceFiles(formKey, value, path, options) {
19
+ if (isFile(value)) {
20
+ return uploadFile(formKey, path || value.name, value, options);
21
+ }
22
+ if (Array.isArray(value)) {
23
+ return Promise.all(value.map((item, index) => replaceFiles(formKey, item, joinPath(path, String(index)), options)));
24
+ }
25
+ if (isPlainObject(value)) {
26
+ const entries = await Promise.all(Object.entries(value).map(async ([key, item]) => [key, await replaceFiles(formKey, item, joinPath(path, key), options)]));
27
+ return Object.fromEntries(entries);
28
+ }
29
+ return value;
30
+ }
31
+ async function uploadFile(formKey, fieldKey, file, options) {
32
+ const resolved = resolveTalizenConfig(options);
33
+ notifyUploadProcess(resolved, fieldKey, 0);
34
+ const preupload = await requestJson(`/form/${formKey}/file/preupload`, {
35
+ method: "POST",
36
+ body: JSON.stringify({
37
+ file_name: file.name,
38
+ hash: await sha256(file),
39
+ mimetype: file.type || "application/octet-stream",
40
+ file_size: file.size,
41
+ }),
42
+ }, options);
43
+ const target = normalizePreuploadResponse(preupload);
44
+ await uploadToSignedUrl(target, file, fieldKey, resolved);
45
+ notifyUploadProcess(resolved, fieldKey, 1);
46
+ return target.fileUrl;
47
+ }
48
+ function normalizePreuploadResponse(response) {
49
+ const hashExist = response.hash_exist === true;
50
+ const uploadUrl = getString(response.presigned_url);
51
+ const fileUrl = getString(response.file_url);
52
+ if (fileUrl == null) {
53
+ throw new Error("Talizen preupload response is missing file_url.");
54
+ }
55
+ if (!hashExist && uploadUrl == null) {
56
+ throw new Error("Talizen preupload response is missing presigned_url.");
57
+ }
58
+ return {
59
+ hashExist,
60
+ uploadUrl,
61
+ fileUrl,
62
+ };
63
+ }
64
+ async function uploadToSignedUrl(target, file, fieldKey, options) {
65
+ if (target.hashExist || target.uploadUrl == null) {
66
+ return;
67
+ }
68
+ if (typeof XMLHttpRequest === "function") {
69
+ await uploadWithXhr(target, file, fieldKey, options);
70
+ return;
71
+ }
72
+ const resolved = resolveTalizenConfig(options);
73
+ const headers = new Headers();
74
+ const body = file;
75
+ if (file.type) {
76
+ headers.set("content-type", file.type);
77
+ }
78
+ const response = await resolved.fetch(target.uploadUrl, {
79
+ method: "PUT",
80
+ headers,
81
+ body,
82
+ signal: resolved.signal,
83
+ });
84
+ if (!response.ok) {
85
+ const text = await response.text();
86
+ throw new Error(`Talizen file upload failed: ${response.status} ${response.statusText} ${text}`.trim());
87
+ }
88
+ }
89
+ function uploadWithXhr(target, file, fieldKey, options) {
90
+ return new Promise((resolve, reject) => {
91
+ const xhr = new XMLHttpRequest();
92
+ const signal = options.signal;
93
+ const body = file;
94
+ xhr.open("PUT", target.uploadUrl ?? "");
95
+ if (file.type) {
96
+ xhr.setRequestHeader("content-type", file.type);
97
+ }
98
+ xhr.upload.onprogress = (event) => {
99
+ if (event.lengthComputable) {
100
+ notifyUploadProcess(options, fieldKey, event.loaded / event.total);
101
+ }
102
+ };
103
+ xhr.onload = () => {
104
+ cleanup();
105
+ if (xhr.status >= 200 && xhr.status < 300) {
106
+ resolve();
107
+ return;
108
+ }
109
+ reject(new Error(`Talizen file upload failed: ${xhr.status} ${xhr.statusText} ${xhr.responseText}`.trim()));
110
+ };
111
+ xhr.onerror = () => {
112
+ cleanup();
113
+ reject(new Error("Talizen file upload failed: network error"));
114
+ };
115
+ xhr.onabort = () => {
116
+ cleanup();
117
+ reject(createAbortError());
118
+ };
119
+ const abort = () => xhr.abort();
120
+ const cleanup = () => signal?.removeEventListener("abort", abort);
121
+ signal?.addEventListener("abort", abort, { once: true });
122
+ xhr.send(body);
123
+ });
124
+ }
125
+ async function sha256(file) {
126
+ const subtle = globalThis.crypto?.subtle;
127
+ if (subtle == null) {
128
+ throw new Error("Talizen file upload requires Web Crypto support.");
129
+ }
130
+ const digest = await subtle.digest("SHA-256", await file.arrayBuffer());
131
+ return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("");
132
+ }
133
+ function notifyUploadProcess(options, key, process) {
134
+ options?.onFileUploadProcess?.(key, clampProcess(process));
135
+ }
136
+ function clampProcess(process) {
137
+ return Math.min(1, Math.max(0, process));
138
+ }
139
+ function joinPath(parent, key) {
140
+ return parent === "" ? key : `${parent}.${key}`;
141
+ }
142
+ function isFile(value) {
143
+ return typeof File === "function" && value instanceof File;
144
+ }
145
+ function isPlainObject(value) {
146
+ return Object.prototype.toString.call(value) === "[object Object]";
147
+ }
148
+ function getString(value) {
149
+ return typeof value === "string" && value !== "" ? value : undefined;
150
+ }
151
+ function createAbortError() {
152
+ if (typeof DOMException === "function") {
153
+ return new DOMException("The operation was aborted.", "AbortError");
154
+ }
155
+ const error = new Error("The operation was aborted.");
156
+ error.name = "AbortError";
157
+ return error;
158
+ }
package/index.d.ts CHANGED
@@ -1,3 +1,73 @@
1
1
  export * from "./core.js";
2
2
  export * from "./cms.js";
3
3
  export * from "./form.js";
4
+ type OneOrMany<T> = T | Array<T>;
5
+ export interface MetadataTitle {
6
+ default?: string;
7
+ template?: string;
8
+ absolute?: string;
9
+ }
10
+ export interface MetadataAuthor {
11
+ name: string;
12
+ url?: string;
13
+ }
14
+ export interface MetadataIconLink {
15
+ url: string;
16
+ media?: string;
17
+ sizes?: string;
18
+ type?: string;
19
+ }
20
+ export interface MetadataOtherIcon {
21
+ rel: string;
22
+ url: string;
23
+ }
24
+ export interface MetadataIcons {
25
+ shortcut?: OneOrMany<string | MetadataIconLink>;
26
+ icon?: OneOrMany<string | MetadataIconLink>;
27
+ apple?: OneOrMany<string | MetadataIconLink>;
28
+ other?: MetadataOtherIcon | Array<MetadataOtherIcon>;
29
+ }
30
+ export interface OpenGraphImage {
31
+ url: string;
32
+ width?: number;
33
+ height?: number;
34
+ alt?: string;
35
+ }
36
+ export interface OpenGraphVideo {
37
+ url: string;
38
+ width?: number;
39
+ height?: number;
40
+ }
41
+ export interface OpenGraphAudio {
42
+ url: string;
43
+ }
44
+ export interface MetadataFormatDetection {
45
+ email?: boolean;
46
+ address?: boolean;
47
+ telephone?: boolean;
48
+ }
49
+ export interface OpenGraphMetadata {
50
+ title?: string;
51
+ description?: string;
52
+ url?: string;
53
+ siteName?: string;
54
+ images?: Array<OpenGraphImage>;
55
+ videos?: Array<OpenGraphVideo>;
56
+ audio?: Array<OpenGraphAudio>;
57
+ locale?: string;
58
+ type?: string;
59
+ }
60
+ export interface Metadata {
61
+ title?: string | MetadataTitle | null;
62
+ description?: string | null;
63
+ generator?: string | null;
64
+ applicationName?: string | null;
65
+ referrer?: string | null;
66
+ keywords?: string | Array<string> | null;
67
+ authors?: Array<MetadataAuthor> | null;
68
+ creator?: string | null;
69
+ publisher?: string | null;
70
+ formatDetection?: MetadataFormatDetection | null;
71
+ openGraph?: OpenGraphMetadata | null;
72
+ icons?: MetadataIcons | null;
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talizen",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Talizen frontend SDK types for cms, form and core.",
5
5
  "type": "module",
6
6
  "license": "MIT",