talizen 0.1.1 → 0.1.3

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
 
@@ -63,6 +66,17 @@ interface Blog extends BaseCmsItem {
63
66
  const blog = await getContent<Blog>("blogs", "hello-world")
64
67
  ```
65
68
 
69
+ ### Get CMS collection metadata
70
+
71
+ ```ts
72
+ import { getContentCollection } from "talizen/cms"
73
+
74
+ const collection = await getContentCollection("blogs")
75
+
76
+ console.log(collection?.title)
77
+ console.log(collection?.jsonSchema)
78
+ ```
79
+
66
80
  ### Submit a form
67
81
 
68
82
  ```ts
@@ -74,6 +88,13 @@ await submitForm("contact-form", {
74
88
  })
75
89
  ```
76
90
 
91
+ When a `File` object appears in the payload, `submitForm()` will:
92
+
93
+ 1. Call `POST /form/:key/file/preupload`
94
+ 2. If `hash_exist` is `false`, upload the file to the returned S3 signed URL
95
+ 3. Replace the original `File` value with the returned `file_url`
96
+ 4. Submit the final payload to `/form/:key/submit`
97
+
77
98
  ## Package Layout
78
99
 
79
100
  - `talizen/core`: shared runtime config, request helpers, and base data types.
package/cms.d.ts CHANGED
@@ -7,7 +7,7 @@ export interface BaseCmsItem {
7
7
  }
8
8
  export interface GetContentListFilterCondition {
9
9
  fieldId?: string;
10
- operator?: string;
10
+ operator?: "eq" | "neq" | "contains" | "not_contains";
11
11
  value?: any;
12
12
  }
13
13
  export interface GetContentListFilter {
@@ -37,10 +37,15 @@ export interface ContentWithPrevNext<T extends BaseCmsItem> {
37
37
  next?: T;
38
38
  prev?: T;
39
39
  }
40
+ export interface ContentCollection {
41
+ jsonSchema?: Record<string, unknown>;
42
+ title?: string;
43
+ }
40
44
  export interface ListResponse<T> {
41
45
  list?: T[];
42
46
  total?: number;
43
47
  }
48
+ export declare function getContentCollection(key: string, options?: TalizenRequestOptions): Promise<ContentCollection | null>;
44
49
  export declare function listContents<T extends BaseCmsItem>(key: T["__cmsKey"], params?: ListContentParams, options?: TalizenRequestOptions): Promise<ListResponse<T>>;
45
50
  export declare function getContent<T extends BaseCmsItem>(key: T["__cmsKey"], slug: string, params?: GetContentParams, options?: TalizenRequestOptions): Promise<T>;
46
51
  export declare function getContentWithPrevNext<T extends BaseCmsItem>(key: T["__cmsKey"], slug: string, params?: GetContentWithPrevNextParams, options?: TalizenRequestOptions): Promise<ContentWithPrevNext<T>>;
package/cms.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { requestJson } from "./core.js";
2
+ export async function getContentCollection(key, options) {
3
+ return requestJson(`/cms/${key}`, undefined, options);
4
+ }
2
5
  export async function listContents(key, params = {}, options) {
3
6
  const response = await requestJson(`/cms/${key}/content_list`, {
4
7
  method: "POST",
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talizen",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Talizen frontend SDK types for cms, form and core.",
5
5
  "type": "module",
6
6
  "license": "MIT",