use rest-api-client

This commit is contained in:
2026-03-12 12:27:16 +08:00
parent d66dae74e8
commit 1e9a01b6c1
4 changed files with 430 additions and 619 deletions

347
AGENTS.md
View File

@@ -1,81 +1,59 @@
# AGENTS.md # AGENTS.md
Kintone Customize Manager 项目开发指南 Kintone Customize Manager - Electron + React 应用,用于管理 Kintone 自定义资源
## 1. 构建命令 ## 1. 构建命令
### 开发
```bash ```bash
# 启动开发服务器HMR + 热重载) # 开发HMR + 热重载)
npm run dev npm run dev
# 类型检查 # 类型检查
npx tsc --noEmit npx tsc --noEmit
```
### 构建 # 构建
```bash
# 构建生产版本
npm run build npm run build
# 打包应用 # 打包
npm run package:win # Windows npm run package:win # Windows
npm run package:mac # macOS npm run package:mac # macOS
npm run package:linux # Linux npm run package:linux # Linux
# 代码质量
npm run lint # ESLint 检查
npm run format # Prettier 格式化
``` ```
### 代码质量 **注意**: 无测试框架,项目暂无测试文件。
```bash ## 2. 项目架构
# ESLint 检查
npm run lint
# 格式化代码
npm run format
```
## 2. 项目结构
``` ```
kintone-customize-manager/ src/
├── src/ ├── main/ # Electron 主进程
│ ├── main/ # Electron 主进程 │ ├── index.ts # 入口,创建窗口
│ ├── index.ts # 主进程入口 │ ├── ipc-handlers.ts # IPC 处理器(所有通信入口
│ ├── ipc-handlers.ts # IPC 通信处理 │ ├── storage.ts # 文件存储 + 密码加密
│ ├── storage.ts # 文件系统操作 └── kintone-api.ts # Kintone REST API 封装
├── kintone-api.ts # Kintone API 封装 ├── preload/ # Preload 脚本
│ ├── updater.ts # 自动更新逻辑 │ ├── index.ts # 暴露 API 到渲染进程
│ └── config.ts # 配置管理 │ └── index.d.ts # 类型声明
│ ├── preload/ # Preload 脚本 ── renderer/ # React 渲染进程
│ │ └── index.ts # 暴露 API 到渲染进程 └── src/
└── renderer/ # React 渲染进程 ├── main.tsx # React 入口
── src/ ── App.tsx # 根组件
├── main.tsx # React 入口 ├── components/ # React 组件
├── App.tsx # 根组件 ├── stores/ # Zustand Stores
── components/ # React 组件 ── types/ # TypeScript 类型
│ ├── hooks/ # 自定义 Hooks
│ ├── stores/ # Zustand Stores
│ ├── utils/ # 工具函数
│ └── types/ # TypeScript 类型
├── resources/ # 应用资源(图标等)
└── build/ # 构建配置
``` ```
## 3. 路径别名 ## 3. 路径别名
| 别名 | 路径 | | 别名 | 路径 |
|------|------| | ------------- | -------------------- |
| `@renderer/*` | `src/renderer/src/*` | | `@renderer/*` | `src/renderer/src/*` |
| `@main/*` | `src/main/*` | | `@main/*` | `src/main/*` |
| `@preload/*` | `src/preload/*` | | `@preload/*` | `src/preload/*` |
使用示例:
```typescript
import { useStore } from '@renderer/stores'
import { ipcHandler } from '@main/ipc-handlers'
```
## 4. 代码风格 ## 4. 代码风格
@@ -83,67 +61,54 @@ import { ipcHandler } from '@main/ipc-handlers'
```typescript ```typescript
// 1. Node.js 内置模块 // 1. Node.js 内置模块
import { join } from 'path' import { join } from "path";
import { app, BrowserWindow } from 'electron' import { app, BrowserWindow } from "electron";
// 2. 第三方库 // 2. 第三方库
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from "react";
import { Button, Layout } from 'antd' import { Button, Layout } from "antd";
// 3. 项目内部模块(使用别名) // 3. 项目内部模块(别名)
import { useDomainStore } from '@renderer/stores' import { useDomainStore } from "@renderer/stores";
import { formatDate } from '@renderer/utils'
// 4. 相对导入 // 4. 相对导入
import './styles.css' import "./styles.css";
``` ```
### 命名规范 ### 命名规范
| 类型 | 规范 | 示例 | - 组件文件/名: `PascalCase` (e.g., `DomainManager.tsx`)
|------|------|------| - 工具函数文件: `camelCase` (e.g., `formatDate.ts`)
| 组件文件 | PascalCase | `DomainManager.tsx` | - Store 文件: `camelCase + Store` (e.g., `domainStore.ts`)
| 工具函数文件 | camelCase | `formatDate.ts` | - 函数/变量: `camelCase` (e.g., `handleSubmit`)
| Store 文件 | camelCase + Store | `domainStore.ts` | - 常量: `UPPER_SNAKE_CASE` (e.g., `MAX_FILE_SIZE`)
| 类型文件 | camelCase | `types.ts` | - 类型/接口: `PascalCase` (e.g., `DomainConfig`)
| 组件名 | PascalCase | `DomainManager` |
| 函数/变量 | camelCase | `handleSubmit` |
| 常量 | UPPER_SNAKE_CASE | `MAX_FILE_SIZE` |
| 类型/接口 | PascalCase | `DomainConfig` |
### TypeScript 规范 ### TypeScript 规范
```typescript ```typescript
// 显式类型定义 // 显式类型定义,避免 any
interface DomainConfig { interface DomainConfig {
id: string id: string;
name: string name: string;
domain: string authType: "password" | "api_token"; // 使用字面量联合类型
username: string
authType: 'password' | 'api_token'
createdAt: string
} }
// 函数返回类型 // 异步函数返回 Promise
function createWindow(): void { } async function fetchDomains(): Promise<Domain[]> {}
// 异步函数 // 使用类型守卫处理 unknown
async function fetchDomains(): Promise<Domain[]> { }
// 避免使用 any使用 unknown 或具体类型
function parseResponse(data: unknown): DomainConfig { function parseResponse(data: unknown): DomainConfig {
// 类型守卫 if (typeof data !== "object" || data === null) {
if (typeof data !== 'object' || data === null) { throw new Error("Invalid response");
throw new Error('Invalid response')
} }
return data as DomainConfig return data as DomainConfig;
} }
``` ```
### React 组件规范 ### React 组件规范
```typescript ```typescript
// 函数组件优先
interface DomainListProps { interface DomainListProps {
domains: Domain[] domains: Domain[]
onSelect: (domain: Domain) => void onSelect: (domain: Domain) => void
@@ -153,19 +118,13 @@ function DomainList({ domains, onSelect }: DomainListProps) {
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
// Hooks 放在组件顶部 // Hooks 放在组件顶部
const { token } = theme.useToken()
// 事件处理函数使用 useCallback // 事件处理函数使用 useCallback
const handleClick = useCallback((domain: Domain) => { const handleClick = useCallback((domain: Domain) => {
setSelectedId(domain.id) setSelectedId(domain.id)
onSelect(domain) onSelect(domain)
}, [onSelect]) }, [onSelect])
return ( return <div>...</div>
<div>
{/* JSX */}
</div>
)
} }
export default DomainList export default DomainList
@@ -174,123 +133,65 @@ export default DomainList
### Zustand Store 规范 ### Zustand Store 规范
```typescript ```typescript
import { create } from 'zustand' import { create } from "zustand";
import { persist } from 'zustand/middleware' import { persist } from "zustand/middleware";
interface DomainState { interface DomainState {
domains: Domain[] domains: Domain[];
currentDomain: Domain | null addDomain: (domain: Domain) => void;
addDomain: (domain: Domain) => void
removeDomain: (id: string) => void
setCurrentDomain: (domain: Domain | null) => void
} }
export const useDomainStore = create<DomainState>()( export const useDomainStore = create<DomainState>()(
persist( persist(
(set) => ({ (set) => ({
domains: [], domains: [],
currentDomain: null, addDomain: (domain) =>
addDomain: (domain) => set((state) => ({ set((state) => ({
domains: [...state.domains, domain] domains: [...state.domains, domain],
})), })),
removeDomain: (id) => set((state) => ({
domains: state.domains.filter(d => d.id !== id)
})),
setCurrentDomain: (domain) => set({ currentDomain: domain })
}), }),
{ name: 'domain-storage' } { name: "domain-storage" },
) ),
) );
``` ```
## 5. 错误处理 ## 5. IPC 通信规范
### 主进程错误处理 ### Result 模式(所有 IPC 返回)
```typescript ```typescript
// IPC 处理错误 type Result<T> = { success: true; data: T } | { success: false; error: string };
ipcMain.handle('fetch-domains', async () => {
try {
const domains = await fetchDomainsFromApi()
return { success: true, data: domains }
} catch (error) {
console.error('Failed to fetch domains:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}
}
})
``` ```
### 渲染进程错误处理
```typescript
// 使用 Result 模式
type Result<T> =
| { success: true; data: T }
| { success: false; error: string }
async function handleFetch(): Promise<Result<Domain[]>> {
try {
const result = await window.api.fetchDomains()
if (!result.success) {
message.error(result.error)
}
return result
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error'
message.error(errorMsg)
return { success: false, error: errorMsg }
}
}
```
## 6. IPC 通信规范
### Preload 暴露 API ### Preload 暴露 API
```typescript ```typescript
// preload/index.ts
const api = { const api = {
// 使用 invoke 进行双向通信 // 双向通信使用 invoke
fetchDomains: () => ipcRenderer.invoke('fetch-domains'), fetchDomains: () => ipcRenderer.invoke("fetch-domains"),
// 单向通信使用 send
notify: (message: string) => ipcRenderer.send("notify", message),
};
// 使用 send 进行单向通信 contextBridge.exposeInMainWorld("api", api);
notify: (message: string) => ipcRenderer.send('notify', message),
// 监听事件
onUpdate: (callback: (info: UpdateInfo) => void) =>
ipcRenderer.on('update-available', (_, info) => callback(info))
}
contextBridge.exposeInMainWorld('api', api)
``` ```
### 类型定义 ### 类型定义
```typescript ```typescript
// preload/index.d.ts // preload/index.d.ts
interface ElectronAPI {
fetchDomains: () => Promise<Result<Domain[]>>
notify: (message: string) => void
onUpdate: (callback: (info: UpdateInfo) => void) => void
}
declare global { declare global {
interface Window { interface Window {
electron: ElectronAPI api: ElectronAPI;
api: ElectronAPI
} }
} }
``` ```
## 7. UI 组件规范 ## 6. UI 组件规范
### 使用 LobeHub UI + Ant Design 使用 **Ant Design 6** + **antd-style**(不用 Tailwind
```typescript ```typescript
// 使用 antd-style 进行样式
import { createStyles } from 'antd-style' import { createStyles } from 'antd-style'
const useStyles = createStyles(({ token, css }) => ({ const useStyles = createStyles(({ token, css }) => ({
@@ -307,59 +208,85 @@ function MyComponent() {
} }
``` ```
### 国际化 国际化:使用中文默认 `import zhCN from 'antd/locale/zh_CN'`
```typescript ## 7. 安全规范
// 使用中文默认
import zhCN from 'antd/locale/zh_CN'
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
```
## 8. 安全规范
### 密码存储 ### 密码存储
```typescript ```typescript
// 使用 safeStorage 加密存储 import { safeStorage } from "electron";
import { safeStorage } from 'electron'
// 加密 // 加密
const encrypted = safeStorage.encryptString(password) const encrypted = safeStorage.encryptString(password);
// 解密 // 解密
const decrypted = safeStorage.decryptString(encrypted) const decrypted = safeStorage.decryptString(encrypted);
``` ```
### CSP 配置 ### WebPreferences
```html 必须启用:`contextIsolation: true`, `nodeIntegration: false`, `sandbox: false`
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; ## 8. 错误处理
script-src 'self';
style-src 'self' 'unsafe-inline'; ```typescript
img-src 'self' data: https:; // 主进程 IPC
font-src 'self' data:;" /> ipcMain.handle("fetch-domains", async () => {
try {
const domains = await fetchDomainsFromApi();
return { success: true, data: domains };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
};
}
});
// 渲染进程调用
const result = await window.api.fetchDomains();
if (!result.success) {
message.error(result.error);
}
``` ```
## 9. fnm 环境配置 ## 9. fnm 环境配置
所有 npm/npx 命令需要先加载 fnm 环境: 所有 npm/npx 命令需加载 fnm 环境:
```bash ```bash
# 方式一:使用 wrapper 脚本
~/.config/opencode/node-fnm-wrapper.sh npm run dev
# 方式二:手动加载
eval "$(fnm env --use-on-cd)" && npm run dev eval "$(fnm env --use-on-cd)" && npm run dev
``` ```
## 10. 注意事项 ## 10. 注意事项
1. **ESM Only**: LobeHub UI 仅支持 ESM,确保 `tsconfig.json``"module": "ESNext"` 1. **ESM Only**: LobeHub UI 仅支持 ESM
2. **React 19**: 必须使用 `@types/react@^19.0.0``@types/react-dom@^19.0.0` 2. **React 19**: 使用 `@types/react@^19.0.0`
3. **CSS 方案**: 使用 `antd-style`不使用 Tailwind CSS 3. **CSS 方案**: 使用 `antd-style`禁止 Tailwind
4. **Context Isolation**: 必须启用 `contextIsolation: true` 4. **禁止 `as any`**: 使用类型守卫或 `unknown`
5. **禁止类型断言**: 避免使用 `as any`,优先使用类型守卫 5. **函数组件优先**: 禁止 class 组件
## 11. MVP Phase - Breaking Changes
**This is MVP phase - breaking changes are acceptable for better design.** However, you MUST:
- **Explain what will break and why**: Document which components/APIs/workflows will be affected
- **Compare old vs new approach**: Show the differences and improvements
- **Document the tradeoffs**: What are the pros and cons of this change
- **Ask for confirmation**: If the change is significant (affects multiple modules or core architecture)
**Examples of acceptable breaking changes during MVP**:
- Refactoring data structures for better type safety
- Changing IPC communication patterns
- Restructuring component hierarchy
- Modifying store architecture
- Updating API interfaces
**Process for breaking changes**:
1. Identify the change and its impact scope
2. Document the breaking change in code comments
3. Explain the reasoning and benefits
4. If significant, ask user for confirmation before implementing
5. Update related documentation after implementation

123
package-lock.json generated
View File

@@ -16,6 +16,7 @@
"@codemirror/view": "^6.36.0", "@codemirror/view": "^6.36.0",
"@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"@kintone/rest-api-client": "^6.1.2",
"@lobehub/ui": "^5.5.0", "@lobehub/ui": "^5.5.0",
"@uiw/react-codemirror": "^4.23.0", "@uiw/react-codemirror": "^4.23.0",
"antd": "^6.1.0", "antd": "^6.1.0",
@@ -2383,6 +2384,35 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@kintone/rest-api-client": {
"version": "6.1.2",
"resolved": "https://registry.npmmirror.com/@kintone/rest-api-client/-/rest-api-client-6.1.2.tgz",
"integrity": "sha512-/6RMKD/cNg8KOfQDXLOTzKkfG+EAKuU6czgVF5b5c4Na9uFB9BENmdg7e1eV3qNwszfIt7UpONfT3apCiw2R3Q==",
"license": "MIT",
"dependencies": {
"axios": "^1.12.2",
"core-js": "^3.46.0",
"form-data": "^4.0.5",
"js-base64": "^3.7.8",
"mime": "^3.0.0",
"qs": "^6.14.2"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@kintone/rest-api-client/node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/mime/-/mime-3.0.0.tgz",
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@lezer/common": { "node_modules/@lezer/common": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.5.1.tgz", "resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.5.1.tgz",
@@ -5909,7 +5939,6 @@
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/at-least-node": { "node_modules/at-least-node": {
@@ -5957,6 +5986,17 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/axios": {
"version": "1.13.6",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-plugin-macros": { "node_modules/babel-plugin-macros": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmmirror.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "resolved": "https://registry.npmmirror.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
@@ -6355,7 +6395,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -6369,7 +6408,6 @@
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
@@ -6707,7 +6745,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
@@ -6841,6 +6878,17 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/core-js": {
"version": "3.48.0",
"resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.48.0.tgz",
"integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": { "node_modules/core-util-is": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz",
@@ -7671,7 +7719,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
@@ -7919,7 +7966,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.1", "call-bind-apply-helpers": "^1.0.1",
@@ -8409,7 +8455,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -8419,7 +8464,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -8458,7 +8502,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0" "es-errors": "^1.3.0"
@@ -8471,7 +8514,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -9253,6 +9295,26 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz",
@@ -9312,7 +9374,6 @@
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
@@ -9487,7 +9548,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
@@ -9512,7 +9572,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dunder-proto": "^1.0.1", "dunder-proto": "^1.0.1",
@@ -9704,7 +9763,6 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -9806,7 +9864,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -9819,7 +9876,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
@@ -10991,6 +11047,12 @@
"jiti": "lib/jiti-cli.mjs" "jiti": "lib/jiti-cli.mjs"
} }
}, },
"node_modules/js-base64": {
"version": "3.7.8",
"resolved": "https://registry.npmmirror.com/js-base64/-/js-base64-3.7.8.tgz",
"integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==",
"license": "BSD-3-Clause"
},
"node_modules/js-cookie": { "node_modules/js-cookie": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz", "resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz",
@@ -11480,7 +11542,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -12684,7 +12745,6 @@
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@@ -12694,7 +12754,6 @@
"version": "2.1.35", "version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"mime-db": "1.52.0" "mime-db": "1.52.0"
@@ -13206,7 +13265,6 @@
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -13890,6 +13948,12 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pump": { "node_modules/pump": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz",
@@ -13910,6 +13974,21 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/qs": {
"version": "6.15.0",
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.0.tgz",
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/query-string": { "node_modules/query-string": {
"version": "9.3.1", "version": "9.3.1",
"resolved": "https://registry.npmmirror.com/query-string/-/query-string-9.3.1.tgz", "resolved": "https://registry.npmmirror.com/query-string/-/query-string-9.3.1.tgz",
@@ -15314,7 +15393,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -15334,7 +15412,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -15351,7 +15428,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bound": "^1.0.2", "call-bound": "^1.0.2",
@@ -15370,7 +15446,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bound": "^1.0.2", "call-bound": "^1.0.2",

View File

@@ -24,6 +24,7 @@
"@codemirror/view": "^6.36.0", "@codemirror/view": "^6.36.0",
"@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"@kintone/rest-api-client": "^6.1.2",
"@lobehub/ui": "^5.5.0", "@lobehub/ui": "^5.5.0",
"@uiw/react-codemirror": "^4.23.0", "@uiw/react-codemirror": "^4.23.0",
"antd": "^6.1.0", "antd": "^6.1.0",

View File

@@ -1,9 +1,5 @@
/** import { KintoneRestAPIClient } from "@kintone/rest-api-client";
* Kintone REST API Client import type { KintoneRestAPIError } from "@kintone/rest-api-client";
* Handles authentication and API calls to Kintone
* Based on REQUIREMENTS.md:331-345, 502-522
*/
import type { DomainWithPassword } from "@renderer/types/domain"; import type { DomainWithPassword } from "@renderer/types/domain";
import type { import type {
KintoneSpace, KintoneSpace,
@@ -12,10 +8,10 @@ import type {
FileContent, FileContent,
AppCustomizationConfig, AppCustomizationConfig,
KintoneApiError, KintoneApiError,
JSFileConfig,
CSSFileConfig,
} from "@renderer/types/kintone"; } from "@renderer/types/kintone";
const REQUEST_TIMEOUT = 30000; // 30 seconds
/** /**
* Custom error class for Kintone API errors * Custom error class for Kintone API errors
*/ */
@@ -41,446 +37,256 @@ export class KintoneError extends Error {
* Kintone REST API Client * Kintone REST API Client
*/ */
export class KintoneClient { export class KintoneClient {
private baseUrl: string; private client: KintoneRestAPIClient;
private headers: Headers;
private domain: string; private domain: string;
constructor(domainConfig: DomainWithPassword) { constructor(domainConfig: DomainWithPassword) {
this.domain = domainConfig.domain; this.domain = domainConfig.domain;
this.baseUrl = `https://${domainConfig.domain}/k/v1/`;
this.headers = new Headers({ const auth =
"Content-Type": "application/json", domainConfig.authType === "api_token"
? { apiToken: domainConfig.apiToken || "" }
: {
username: domainConfig.username,
password: domainConfig.password,
};
this.client = new KintoneRestAPIClient({
baseUrl: `https://${domainConfig.domain}`,
auth,
}); });
if (domainConfig.authType === "api_token") {
// API Token authentication
this.headers.set("X-Cybozu-API-Token", domainConfig.apiToken || "");
} else {
// Password authentication (Basic Auth)
const credentials = Buffer.from(
`${domainConfig.username}:${domainConfig.password}`,
).toString("base64");
this.headers.set("X-Cybozu-Authorization", credentials);
}
} }
/** private convertError(error: unknown): KintoneError {
* Make an API request with timeout if (error && typeof error === "object" && "code" in error) {
*/ const apiError = error as KintoneRestAPIError;
private async request<T>( return new KintoneError(
endpoint: string, apiError.message,
options: RequestInit = {}, { code: apiError.code, message: apiError.message, id: apiError.id },
): Promise<T> { apiError.status,
const controller = new AbortController(); );
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT); }
if (error instanceof Error) {
return new KintoneError(error.message);
}
return new KintoneError("Unknown error occurred");
}
private async withErrorHandling<T>(operation: () => Promise<T>): Promise<T> {
try { try {
const url = this.baseUrl + endpoint; return await operation();
const response = await fetch(url, {
...options,
headers: this.headers,
signal: controller.signal,
});
const data = await response.json();
if (!response.ok) {
const apiError = data as KintoneApiError;
throw new KintoneError(
apiError.message || `API request failed: ${response.status}`,
apiError,
response.status,
);
}
return data as T;
} catch (error) { } catch (error) {
if (error instanceof KintoneError) { throw this.convertError(error);
throw error;
}
if (error instanceof Error) {
if (error.name === "AbortError") {
throw new KintoneError("Request timeout");
}
throw new KintoneError(`Network error: ${error.message}`);
}
throw new KintoneError("Unknown error occurred");
} finally {
clearTimeout(timeoutId);
} }
} }
// ==================== Space APIs ==================== private mapSpace(space: {
id: string | number;
/** name: string;
* Get list of spaces code: string;
* GET /k/v1/space.json createdAt?: string;
*/ creator?: { code: string; name: string };
async getSpaces(): Promise<KintoneSpace[]> { }): KintoneSpace {
interface SpacesResponse { return {
spaces: Array<{ id: String(space.id),
id: string;
name: string;
code: string;
createdAt?: string;
creator?: { code: string; name: string };
}>;
}
const response = await this.request<SpacesResponse>("space.json");
return response.spaces.map((space) => ({
id: space.id,
name: space.name, name: space.name,
code: space.code, code: space.code,
createdAt: space.createdAt, createdAt: space.createdAt,
creator: space.creator, creator: space.creator
})); ? { code: space.creator.code, name: space.creator.name }
: undefined,
};
} }
// ==================== App APIs ==================== private mapApp(app: {
appId: string | number;
/** name: string;
* Get list of apps, optionally filtered by space code?: string;
* GET /k/v1/apps.json?space={spaceId} spaceId?: string | number;
*/ createdAt: string;
async getApps(spaceId?: string): Promise<KintoneApp[]> { creator?: { code: string; name: string };
interface AppsResponse { modifiedAt?: string;
apps: Array<{ modifier?: { code: string; name: string };
appId: string; }): KintoneApp {
name: string; return {
code?: string; appId: String(app.appId),
spaceId?: string;
createdAt: string;
creator?: { code: string; name: string };
modifiedAt?: string;
modifier?: { code: string; name: string };
}>;
}
let endpoint = "apps.json";
if (spaceId) {
endpoint += `?space=${spaceId}`;
}
const response = await this.request<AppsResponse>(endpoint);
return response.apps.map((app) => ({
appId: app.appId,
name: app.name, name: app.name,
code: app.code, code: app.code,
spaceId: app.spaceId, spaceId: app.spaceId ? String(app.spaceId) : undefined,
createdAt: app.createdAt, createdAt: app.createdAt,
creator: app.creator, creator: app.creator,
modifiedAt: app.modifiedAt, modifiedAt: app.modifiedAt,
modifier: app.modifier, modifier: app.modifier,
})); };
} }
/** private mapResource(resource: {
* Get app details including customization config type: string;
* GET /k/v1/app.json?app={appId} file?: { fileKey: string; name: string };
*/ url?: string;
async getAppDetail(appId: string): Promise<AppDetail> { }): JSFileConfig | CSSFileConfig {
interface AppResponse { return {
appId: string; type: resource.type as "FILE" | "URL",
name: string; file: resource.file
code?: string; ? { fileKey: resource.file.fileKey, name: resource.file.name, size: 0 }
description?: string; : undefined,
spaceId?: string; url: resource.url,
createdAt: string;
creator: { code: string; name: string };
modifiedAt: string;
modifier: { code: string; name: string };
}
interface AppCustomizeResponse {
appId: string;
scope: string;
desktop: {
js?: Array<{
type: string;
file?: { fileKey: string; name: string };
url?: string;
}>;
css?: Array<{
type: string;
file?: { fileKey: string; name: string };
url?: string;
}>;
};
mobile: {
js?: Array<{
type: string;
file?: { fileKey: string; name: string };
url?: string;
}>;
css?: Array<{
type: string;
file?: { fileKey: string; name: string };
url?: string;
}>;
};
}
// Get basic app info
const appInfo = await this.request<AppResponse>(`app.json?app=${appId}`);
// Get customization config
const customizeInfo = await this.request<AppCustomizeResponse>(
`app/customize.json?app=${appId}`,
);
// Transform customization config
const customization: AppCustomizationConfig = {
javascript: {
pc:
customizeInfo.desktop?.js?.map((js) => ({
type: js.type as "FILE" | "URL",
file: js.file
? { fileKey: js.file.fileKey, name: js.file.name, size: 0 }
: undefined,
url: js.url,
})) || [],
mobile:
customizeInfo.mobile?.js?.map((js) => ({
type: js.type as "FILE" | "URL",
file: js.file
? { fileKey: js.file.fileKey, name: js.file.name, size: 0 }
: undefined,
url: js.url,
})) || [],
},
stylesheet: {
pc:
customizeInfo.desktop?.css?.map((css) => ({
type: css.type as "FILE" | "URL",
file: css.file
? { fileKey: css.file.fileKey, name: css.file.name, size: 0 }
: undefined,
url: css.url,
})) || [],
mobile:
customizeInfo.mobile?.css?.map((css) => ({
type: css.type as "FILE" | "URL",
file: css.file
? { fileKey: css.file.fileKey, name: css.file.name, size: 0 }
: undefined,
url: css.url,
})) || [],
},
plugins: [],
}; };
}
private buildCustomizeSection(
js?: JSFileConfig[],
css?: CSSFileConfig[],
):
| { js?: CustomizeResourceItem[]; css?: CustomizeResourceItem[] }
| undefined {
if (!js && !css) return undefined;
return { return {
appId: appInfo.appId, js: js?.map((item) => ({
name: appInfo.name, type: item.type,
code: appInfo.code, file: item.file ? { fileKey: item.file.fileKey } : undefined,
description: appInfo.description, url: item.url,
spaceId: appInfo.spaceId, })),
createdAt: appInfo.createdAt, css: css?.map((item) => ({
creator: appInfo.creator, type: item.type,
modifiedAt: appInfo.modifiedAt, file: item.file ? { fileKey: item.file.fileKey } : undefined,
modifier: appInfo.modifier, url: item.url,
customization, })),
}; };
} }
// ==================== Space APIs ====================
async getSpaces(): Promise<KintoneSpace[]> {
return this.withErrorHandling(async () => {
const response = await this.client.space.getSpaces();
return response.spaces.map((space) => this.mapSpace(space));
});
}
// ==================== App APIs ====================
async getApps(spaceId?: string): Promise<KintoneApp[]> {
return this.withErrorHandling(async () => {
const params = spaceId ? { spaceId } : {};
const response = await this.client.app.getApps(params);
return response.apps.map((app) => this.mapApp(app));
});
}
async getAppDetail(appId: string): Promise<AppDetail> {
return this.withErrorHandling(async () => {
const [appInfo, customizeInfo] = await Promise.all([
this.client.app.getApp({ id: appId }),
this.client.app.getAppCustomize({ app: appId }),
]);
const customization: AppCustomizationConfig = {
javascript: {
pc:
customizeInfo.desktop?.js?.map((js) => this.mapResource(js)) || [],
mobile:
customizeInfo.mobile?.js?.map((js) => this.mapResource(js)) || [],
},
stylesheet: {
pc:
customizeInfo.desktop?.css?.map((css) => this.mapResource(css)) ||
[],
mobile:
customizeInfo.mobile?.css?.map((css) => this.mapResource(css)) ||
[],
},
plugins: [],
};
return {
appId: String(appInfo.appId),
name: appInfo.name,
code: appInfo.code,
description: appInfo.description,
spaceId: appInfo.spaceId ? String(appInfo.spaceId) : undefined,
createdAt: appInfo.createdAt,
creator: appInfo.creator,
modifiedAt: appInfo.modifiedAt,
modifier: appInfo.modifier,
customization,
};
});
}
// ==================== File APIs ==================== // ==================== File APIs ====================
/**
* Get file content from Kintone
* GET /k/v1/file.json?fileKey={fileKey}
*/
async getFileContent(fileKey: string): Promise<FileContent> { async getFileContent(fileKey: string): Promise<FileContent> {
const controller = new AbortController(); return this.withErrorHandling(async () => {
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT); const data = await this.client.file.downloadFile({ fileKey });
const buffer = Buffer.from(data);
try { const content = buffer.toString("base64");
const url = `${this.baseUrl}file.json?fileKey=${fileKey}`;
const response = await fetch(url, {
headers: this.headers,
signal: controller.signal,
});
if (!response.ok) {
const error = await response.json();
throw new KintoneError(
error.message || "Failed to get file",
error,
response.status,
);
}
const contentType =
response.headers.get("content-type") || "application/octet-stream";
const arrayBuffer = await response.arrayBuffer();
const content = Buffer.from(arrayBuffer).toString("base64");
return { return {
fileKey, fileKey,
name: fileKey, // Kintone doesn't return filename in file API name: fileKey,
size: arrayBuffer.byteLength, size: buffer.byteLength,
mimeType: contentType, mimeType: "application/octet-stream",
content, content,
}; };
} finally { });
clearTimeout(timeoutId);
}
} }
/**
* Upload a file to Kintone
* POST /k/v1/file.json (multipart/form-data)
*/
async uploadFile( async uploadFile(
content: string | Buffer, content: string | Buffer,
fileName: string, fileName: string,
mimeType: string = "application/javascript", _mimeType?: string,
): Promise<{ fileKey: string }> { ): Promise<{ fileKey: string }> {
const controller = new AbortController(); return this.withErrorHandling(async () => {
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT); const response = await this.client.file.uploadFile({
file: { name: fileName, data: content },
try {
const url = `${this.baseUrl}file.json`;
// Create form data
const formData = new FormData();
const blob = new Blob(
[typeof content === "string" ? content : Buffer.from(content)],
{ type: mimeType },
);
formData.append("file", blob, fileName);
// Remove Content-Type header to let browser set it with boundary
const uploadHeaders = new Headers(this.headers);
uploadHeaders.delete("Content-Type");
const response = await fetch(url, {
method: "POST",
headers: uploadHeaders,
body: formData,
signal: controller.signal,
}); });
return { fileKey: response.fileKey };
if (!response.ok) { });
const error = await response.json();
throw new KintoneError(
error.message || "Failed to upload file",
error,
response.status,
);
}
const data = await response.json();
return { fileKey: data.fileKey };
} finally {
clearTimeout(timeoutId);
}
} }
// ==================== Deploy APIs ==================== // ==================== Deploy APIs ====================
/**
* Update app customization config
* PUT /k/v1/app/customize.json
*/
async updateAppCustomize( async updateAppCustomize(
appId: string, appId: string,
config: AppCustomizationConfig, config: AppCustomizationConfig,
): Promise<void> { ): Promise<void> {
interface CustomizeUpdateRequest { return this.withErrorHandling(async () => {
app: string; await this.client.app.updateAppCustomize({
desktop?: { app: appId,
js?: Array<{ type: string; file?: { fileKey: string }; url?: string }>; desktop: this.buildCustomizeSection(
css?: Array<{ type: string; file?: { fileKey: string }; url?: string }>; config.javascript?.pc,
}; config.stylesheet?.pc,
mobile?: { ),
js?: Array<{ type: string; file?: { fileKey: string }; url?: string }>; mobile: this.buildCustomizeSection(
css?: Array<{ type: string; file?: { fileKey: string }; url?: string }>; config.javascript?.mobile,
}; config.stylesheet?.mobile,
} ),
scope: "ALL",
const requestBody: CustomizeUpdateRequest = { });
app: appId,
desktop: {
js: config.javascript?.pc
?.map((js) => ({
type: js.type,
file: js.file ? { fileKey: js.file.fileKey } : undefined,
url: js.url,
}))
.filter(Boolean),
css: config.stylesheet?.pc
?.map((css) => ({
type: css.type,
file: css.file ? { fileKey: css.file.fileKey } : undefined,
url: css.url,
}))
.filter(Boolean),
},
mobile: {
js: config.javascript?.mobile
?.map((js) => ({
type: js.type,
file: js.file ? { fileKey: js.file.fileKey } : undefined,
url: js.url,
}))
.filter(Boolean),
css: config.stylesheet?.mobile
?.map((css) => ({
type: css.type,
file: css.file ? { fileKey: css.file.fileKey } : undefined,
url: css.url,
}))
.filter(Boolean),
},
};
await this.request("app/customize.json", {
method: "PUT",
body: JSON.stringify(requestBody),
}); });
} }
/**
* Deploy app changes
* POST /k/v1/preview/app/deploy.json
*/
async deployApp(appId: string): Promise<void> { async deployApp(appId: string): Promise<void> {
await this.request("preview/app/deploy.json", { return this.withErrorHandling(async () => {
method: "POST", await this.client.app.deployApp({ apps: [{ app: appId }] });
body: JSON.stringify({ apps: [{ app: appId }] }),
}); });
} }
/**
* Get deploy status
* GET /k/v1/preview/app/deploy.json?app={appId}
*/
async getDeployStatus( async getDeployStatus(
appId: string, appId: string,
): Promise<"PROCESSING" | "SUCCESS" | "FAIL" | "CANCEL"> { ): Promise<"PROCESSING" | "SUCCESS" | "FAIL" | "CANCEL"> {
interface DeployStatusResponse { return this.withErrorHandling(async () => {
apps: Array<{ const response = await this.client.app.getDeployStatus({ app: appId });
app: string; return response.apps[0]?.status || "FAIL";
status: "PROCESSING" | "SUCCESS" | "FAIL" | "CANCEL"; });
}>;
}
const response = await this.request<DeployStatusResponse>(
`preview/app/deploy.json?app=${appId}`,
);
const appStatus = response.apps.find((a) => a.app === appId);
return appStatus?.status || "FAIL";
} }
// ==================== Utility Methods ==================== // ==================== Utility Methods ====================
/**
* Test connection to Kintone
*/
async testConnection(): Promise<{ success: boolean; error?: string }> { async testConnection(): Promise<{ success: boolean; error?: string }> {
try { try {
await this.getApps(); await this.getApps();
@@ -494,15 +300,17 @@ export class KintoneClient {
} }
} }
/**
* Get the domain name
*/
getDomain(): string { getDomain(): string {
return this.domain; return this.domain;
} }
} }
// Export factory function for convenience type CustomizeResourceItem = {
type: string;
file?: { fileKey: string };
url?: string;
};
export function createKintoneClient(domain: DomainWithPassword): KintoneClient { export function createKintoneClient(domain: DomainWithPassword): KintoneClient {
return new KintoneClient(domain); return new KintoneClient(domain);
} }