use rest-api-client
This commit is contained in:
337
AGENTS.md
337
AGENTS.md
@@ -1,149 +1,114 @@
|
||||
# AGENTS.md
|
||||
|
||||
Kintone Customize Manager 项目开发指南。
|
||||
Kintone Customize Manager - Electron + React 应用,用于管理 Kintone 自定义资源。
|
||||
|
||||
## 1. 构建命令
|
||||
|
||||
### 开发
|
||||
|
||||
```bash
|
||||
# 启动开发服务器(HMR + 热重载)
|
||||
# 开发(HMR + 热重载)
|
||||
npm run dev
|
||||
|
||||
# 类型检查
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
### 构建
|
||||
|
||||
```bash
|
||||
# 构建生产版本
|
||||
# 构建
|
||||
npm run build
|
||||
|
||||
# 打包应用
|
||||
# 打包
|
||||
npm run package:win # Windows
|
||||
npm run package:mac # macOS
|
||||
npm run package:linux # Linux
|
||||
|
||||
# 代码质量
|
||||
npm run lint # ESLint 检查
|
||||
npm run format # Prettier 格式化
|
||||
```
|
||||
|
||||
### 代码质量
|
||||
**注意**: 无测试框架,项目暂无测试文件。
|
||||
|
||||
```bash
|
||||
# ESLint 检查
|
||||
npm run lint
|
||||
|
||||
# 格式化代码
|
||||
npm run format
|
||||
```
|
||||
|
||||
## 2. 项目结构
|
||||
## 2. 项目架构
|
||||
|
||||
```
|
||||
kintone-customize-manager/
|
||||
├── src/
|
||||
│ ├── main/ # Electron 主进程
|
||||
│ │ ├── index.ts # 主进程入口
|
||||
│ │ ├── ipc-handlers.ts # IPC 通信处理
|
||||
│ │ ├── storage.ts # 文件系统操作
|
||||
│ │ ├── kintone-api.ts # Kintone API 封装
|
||||
│ │ ├── updater.ts # 自动更新逻辑
|
||||
│ │ └── config.ts # 配置管理
|
||||
│ ├── preload/ # Preload 脚本
|
||||
│ │ └── index.ts # 暴露 API 到渲染进程
|
||||
│ └── renderer/ # React 渲染进程
|
||||
│ └── src/
|
||||
│ ├── main.tsx # React 入口
|
||||
│ ├── App.tsx # 根组件
|
||||
│ ├── components/ # React 组件
|
||||
│ ├── hooks/ # 自定义 Hooks
|
||||
│ ├── stores/ # Zustand Stores
|
||||
│ ├── utils/ # 工具函数
|
||||
│ └── types/ # TypeScript 类型
|
||||
├── resources/ # 应用资源(图标等)
|
||||
└── build/ # 构建配置
|
||||
src/
|
||||
├── main/ # Electron 主进程
|
||||
│ ├── index.ts # 入口,创建窗口
|
||||
│ ├── ipc-handlers.ts # IPC 处理器(所有通信入口)
|
||||
│ ├── storage.ts # 文件存储 + 密码加密
|
||||
│ └── kintone-api.ts # Kintone REST API 封装
|
||||
├── preload/ # Preload 脚本
|
||||
│ ├── index.ts # 暴露 API 到渲染进程
|
||||
│ └── index.d.ts # 类型声明
|
||||
└── renderer/ # React 渲染进程
|
||||
└── src/
|
||||
├── main.tsx # React 入口
|
||||
├── App.tsx # 根组件
|
||||
├── components/ # React 组件
|
||||
├── stores/ # Zustand Stores
|
||||
└── types/ # TypeScript 类型
|
||||
```
|
||||
|
||||
## 3. 路径别名
|
||||
|
||||
| 别名 | 路径 |
|
||||
|------|------|
|
||||
| ------------- | -------------------- |
|
||||
| `@renderer/*` | `src/renderer/src/*` |
|
||||
| `@main/*` | `src/main/*` |
|
||||
| `@preload/*` | `src/preload/*` |
|
||||
|
||||
使用示例:
|
||||
```typescript
|
||||
import { useStore } from '@renderer/stores'
|
||||
import { ipcHandler } from '@main/ipc-handlers'
|
||||
```
|
||||
|
||||
## 4. 代码风格
|
||||
|
||||
### 导入顺序
|
||||
|
||||
```typescript
|
||||
// 1. Node.js 内置模块
|
||||
import { join } from 'path'
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
import { join } from "path";
|
||||
import { app, BrowserWindow } from "electron";
|
||||
|
||||
// 2. 第三方库
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Button, Layout } from 'antd'
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button, Layout } from "antd";
|
||||
|
||||
// 3. 项目内部模块(使用别名)
|
||||
import { useDomainStore } from '@renderer/stores'
|
||||
import { formatDate } from '@renderer/utils'
|
||||
// 3. 项目内部模块(别名)
|
||||
import { useDomainStore } from "@renderer/stores";
|
||||
|
||||
// 4. 相对导入
|
||||
import './styles.css'
|
||||
import "./styles.css";
|
||||
```
|
||||
|
||||
### 命名规范
|
||||
|
||||
| 类型 | 规范 | 示例 |
|
||||
|------|------|------|
|
||||
| 组件文件 | PascalCase | `DomainManager.tsx` |
|
||||
| 工具函数文件 | camelCase | `formatDate.ts` |
|
||||
| Store 文件 | camelCase + Store | `domainStore.ts` |
|
||||
| 类型文件 | camelCase | `types.ts` |
|
||||
| 组件名 | PascalCase | `DomainManager` |
|
||||
| 函数/变量 | camelCase | `handleSubmit` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `MAX_FILE_SIZE` |
|
||||
| 类型/接口 | PascalCase | `DomainConfig` |
|
||||
- 组件文件/名: `PascalCase` (e.g., `DomainManager.tsx`)
|
||||
- 工具函数文件: `camelCase` (e.g., `formatDate.ts`)
|
||||
- Store 文件: `camelCase + Store` (e.g., `domainStore.ts`)
|
||||
- 函数/变量: `camelCase` (e.g., `handleSubmit`)
|
||||
- 常量: `UPPER_SNAKE_CASE` (e.g., `MAX_FILE_SIZE`)
|
||||
- 类型/接口: `PascalCase` (e.g., `DomainConfig`)
|
||||
|
||||
### TypeScript 规范
|
||||
|
||||
```typescript
|
||||
// 显式类型定义
|
||||
// 显式类型定义,避免 any
|
||||
interface DomainConfig {
|
||||
id: string
|
||||
name: string
|
||||
domain: string
|
||||
username: string
|
||||
authType: 'password' | 'api_token'
|
||||
createdAt: string
|
||||
id: string;
|
||||
name: string;
|
||||
authType: "password" | "api_token"; // 使用字面量联合类型
|
||||
}
|
||||
|
||||
// 函数返回类型
|
||||
function createWindow(): void { }
|
||||
|
||||
// 异步函数
|
||||
// 异步函数返回 Promise
|
||||
async function fetchDomains(): Promise<Domain[]> {}
|
||||
|
||||
// 避免使用 any,使用 unknown 或具体类型
|
||||
// 使用类型守卫处理 unknown
|
||||
function parseResponse(data: unknown): DomainConfig {
|
||||
// 类型守卫
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
throw new Error('Invalid response')
|
||||
if (typeof data !== "object" || data === null) {
|
||||
throw new Error("Invalid response");
|
||||
}
|
||||
return data as DomainConfig
|
||||
return data as DomainConfig;
|
||||
}
|
||||
```
|
||||
|
||||
### React 组件规范
|
||||
|
||||
```typescript
|
||||
// 函数组件优先
|
||||
interface DomainListProps {
|
||||
domains: Domain[]
|
||||
onSelect: (domain: Domain) => void
|
||||
@@ -153,19 +118,13 @@ function DomainList({ domains, onSelect }: DomainListProps) {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
|
||||
// Hooks 放在组件顶部
|
||||
const { token } = theme.useToken()
|
||||
|
||||
// 事件处理函数使用 useCallback
|
||||
const handleClick = useCallback((domain: Domain) => {
|
||||
setSelectedId(domain.id)
|
||||
onSelect(domain)
|
||||
}, [onSelect])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* JSX */}
|
||||
</div>
|
||||
)
|
||||
return <div>...</div>
|
||||
}
|
||||
|
||||
export default DomainList
|
||||
@@ -174,123 +133,65 @@ export default DomainList
|
||||
### Zustand Store 规范
|
||||
|
||||
```typescript
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
interface DomainState {
|
||||
domains: Domain[]
|
||||
currentDomain: Domain | null
|
||||
addDomain: (domain: Domain) => void
|
||||
removeDomain: (id: string) => void
|
||||
setCurrentDomain: (domain: Domain | null) => void
|
||||
domains: Domain[];
|
||||
addDomain: (domain: Domain) => void;
|
||||
}
|
||||
|
||||
export const useDomainStore = create<DomainState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
domains: [],
|
||||
currentDomain: null,
|
||||
addDomain: (domain) => set((state) => ({
|
||||
domains: [...state.domains, domain]
|
||||
addDomain: (domain) =>
|
||||
set((state) => ({
|
||||
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
|
||||
// IPC 处理错误
|
||||
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'
|
||||
}
|
||||
}
|
||||
})
|
||||
type Result<T> = { success: true; data: T } | { success: false; error: string };
|
||||
```
|
||||
|
||||
### 渲染进程错误处理
|
||||
|
||||
```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
|
||||
|
||||
```typescript
|
||||
// preload/index.ts
|
||||
const api = {
|
||||
// 使用 invoke 进行双向通信
|
||||
fetchDomains: () => ipcRenderer.invoke('fetch-domains'),
|
||||
// 双向通信使用 invoke
|
||||
fetchDomains: () => ipcRenderer.invoke("fetch-domains"),
|
||||
// 单向通信使用 send
|
||||
notify: (message: string) => ipcRenderer.send("notify", message),
|
||||
};
|
||||
|
||||
// 使用 send 进行单向通信
|
||||
notify: (message: string) => ipcRenderer.send('notify', message),
|
||||
|
||||
// 监听事件
|
||||
onUpdate: (callback: (info: UpdateInfo) => void) =>
|
||||
ipcRenderer.on('update-available', (_, info) => callback(info))
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('api', api)
|
||||
contextBridge.exposeInMainWorld("api", api);
|
||||
```
|
||||
|
||||
### 类型定义
|
||||
|
||||
```typescript
|
||||
// preload/index.d.ts
|
||||
interface ElectronAPI {
|
||||
fetchDomains: () => Promise<Result<Domain[]>>
|
||||
notify: (message: string) => void
|
||||
onUpdate: (callback: (info: UpdateInfo) => void) => void
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI
|
||||
api: ElectronAPI
|
||||
api: ElectronAPI;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. UI 组件规范
|
||||
## 6. UI 组件规范
|
||||
|
||||
### 使用 LobeHub UI + Ant Design
|
||||
使用 **Ant Design 6** + **antd-style**(不用 Tailwind):
|
||||
|
||||
```typescript
|
||||
// 使用 antd-style 进行样式
|
||||
import { createStyles } from 'antd-style'
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
@@ -307,59 +208,85 @@ function MyComponent() {
|
||||
}
|
||||
```
|
||||
|
||||
### 国际化
|
||||
国际化:使用中文默认 `import zhCN from 'antd/locale/zh_CN'`
|
||||
|
||||
```typescript
|
||||
// 使用中文默认
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<App />
|
||||
</ConfigProvider>
|
||||
```
|
||||
|
||||
## 8. 安全规范
|
||||
## 7. 安全规范
|
||||
|
||||
### 密码存储
|
||||
|
||||
```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
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self';
|
||||
script-src 'self';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: https:;
|
||||
font-src 'self' data:;" />
|
||||
必须启用:`contextIsolation: true`, `nodeIntegration: false`, `sandbox: false`
|
||||
|
||||
## 8. 错误处理
|
||||
|
||||
```typescript
|
||||
// 主进程 IPC
|
||||
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 环境配置
|
||||
|
||||
所有 npm/npx 命令需要先加载 fnm 环境:
|
||||
所有 npm/npx 命令需加载 fnm 环境:
|
||||
|
||||
```bash
|
||||
# 方式一:使用 wrapper 脚本
|
||||
~/.config/opencode/node-fnm-wrapper.sh npm run dev
|
||||
|
||||
# 方式二:手动加载
|
||||
eval "$(fnm env --use-on-cd)" && npm run dev
|
||||
```
|
||||
|
||||
## 10. 注意事项
|
||||
|
||||
1. **ESM Only**: LobeHub UI 仅支持 ESM,确保 `tsconfig.json` 中 `"module": "ESNext"`
|
||||
2. **React 19**: 必须使用 `@types/react@^19.0.0` 和 `@types/react-dom@^19.0.0`
|
||||
3. **CSS 方案**: 使用 `antd-style`,不使用 Tailwind CSS
|
||||
4. **Context Isolation**: 必须启用 `contextIsolation: true`
|
||||
5. **禁止类型断言**: 避免使用 `as any`,优先使用类型守卫
|
||||
1. **ESM Only**: LobeHub UI 仅支持 ESM
|
||||
2. **React 19**: 使用 `@types/react@^19.0.0`
|
||||
3. **CSS 方案**: 使用 `antd-style`,禁止 Tailwind
|
||||
4. **禁止 `as any`**: 使用类型守卫或 `unknown`
|
||||
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
123
package-lock.json
generated
@@ -16,6 +16,7 @@
|
||||
"@codemirror/view": "^6.36.0",
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@kintone/rest-api-client": "^6.1.2",
|
||||
"@lobehub/ui": "^5.5.0",
|
||||
"@uiw/react-codemirror": "^4.23.0",
|
||||
"antd": "^6.1.0",
|
||||
@@ -2383,6 +2384,35 @@
|
||||
"@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": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.5.1.tgz",
|
||||
@@ -5909,7 +5939,6 @@
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/at-least-node": {
|
||||
@@ -5957,6 +5986,17 @@
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
||||
@@ -6355,7 +6395,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -6369,7 +6408,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
@@ -6707,7 +6745,6 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
@@ -6841,6 +6878,17 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
@@ -7671,7 +7719,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
@@ -7919,7 +7966,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
@@ -8409,7 +8455,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -8419,7 +8464,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -8458,7 +8502,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
@@ -8471,7 +8514,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -9253,6 +9295,26 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz",
|
||||
@@ -9312,7 +9374,6 @@
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
@@ -9487,7 +9548,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
@@ -9512,7 +9572,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
@@ -9704,7 +9763,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -9806,7 +9864,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -9819,7 +9876,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
@@ -10991,6 +11047,12 @@
|
||||
"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": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||
@@ -11480,7 +11542,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -12684,7 +12745,6 @@
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -12694,7 +12754,6 @@
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
@@ -13206,7 +13265,6 @@
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -13890,6 +13948,12 @@
|
||||
"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": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz",
|
||||
@@ -13910,6 +13974,21 @@
|
||||
"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": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/query-string/-/query-string-9.3.1.tgz",
|
||||
@@ -15314,7 +15393,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -15334,7 +15412,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -15351,7 +15428,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
@@ -15370,7 +15446,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@codemirror/view": "^6.36.0",
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@kintone/rest-api-client": "^6.1.2",
|
||||
"@lobehub/ui": "^5.5.0",
|
||||
"@uiw/react-codemirror": "^4.23.0",
|
||||
"antd": "^6.1.0",
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
/**
|
||||
* Kintone REST API Client
|
||||
* Handles authentication and API calls to Kintone
|
||||
* Based on REQUIREMENTS.md:331-345, 502-522
|
||||
*/
|
||||
|
||||
import { KintoneRestAPIClient } from "@kintone/rest-api-client";
|
||||
import type { KintoneRestAPIError } from "@kintone/rest-api-client";
|
||||
import type { DomainWithPassword } from "@renderer/types/domain";
|
||||
import type {
|
||||
KintoneSpace,
|
||||
@@ -12,10 +8,10 @@ import type {
|
||||
FileContent,
|
||||
AppCustomizationConfig,
|
||||
KintoneApiError,
|
||||
JSFileConfig,
|
||||
CSSFileConfig,
|
||||
} from "@renderer/types/kintone";
|
||||
|
||||
const REQUEST_TIMEOUT = 30000; // 30 seconds
|
||||
|
||||
/**
|
||||
* Custom error class for Kintone API errors
|
||||
*/
|
||||
@@ -41,446 +37,256 @@ export class KintoneError extends Error {
|
||||
* Kintone REST API Client
|
||||
*/
|
||||
export class KintoneClient {
|
||||
private baseUrl: string;
|
||||
private headers: Headers;
|
||||
private client: KintoneRestAPIClient;
|
||||
private domain: string;
|
||||
|
||||
constructor(domainConfig: DomainWithPassword) {
|
||||
this.domain = domainConfig.domain;
|
||||
this.baseUrl = `https://${domainConfig.domain}/k/v1/`;
|
||||
this.headers = new Headers({
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
const auth =
|
||||
domainConfig.authType === "api_token"
|
||||
? { apiToken: domainConfig.apiToken || "" }
|
||||
: {
|
||||
username: domainConfig.username,
|
||||
password: domainConfig.password,
|
||||
};
|
||||
|
||||
this.client = new KintoneRestAPIClient({
|
||||
baseUrl: `https://${domainConfig.domain}`,
|
||||
auth,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an API request with timeout
|
||||
*/
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {},
|
||||
): Promise<T> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
|
||||
|
||||
try {
|
||||
const url = this.baseUrl + endpoint;
|
||||
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,
|
||||
private convertError(error: unknown): KintoneError {
|
||||
if (error && typeof error === "object" && "code" in error) {
|
||||
const apiError = error as KintoneRestAPIError;
|
||||
return new KintoneError(
|
||||
apiError.message,
|
||||
{ code: apiError.code, message: apiError.message, id: apiError.id },
|
||||
apiError.status,
|
||||
);
|
||||
}
|
||||
|
||||
return data as T;
|
||||
} catch (error) {
|
||||
if (error instanceof KintoneError) {
|
||||
throw error;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
if (error.name === "AbortError") {
|
||||
throw new KintoneError("Request timeout");
|
||||
return new KintoneError(error.message);
|
||||
}
|
||||
throw new KintoneError(`Network error: ${error.message}`);
|
||||
|
||||
return new KintoneError("Unknown error occurred");
|
||||
}
|
||||
throw new KintoneError("Unknown error occurred");
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
private async withErrorHandling<T>(operation: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
throw this.convertError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Space APIs ====================
|
||||
|
||||
/**
|
||||
* Get list of spaces
|
||||
* GET /k/v1/space.json
|
||||
*/
|
||||
async getSpaces(): Promise<KintoneSpace[]> {
|
||||
interface SpacesResponse {
|
||||
spaces: Array<{
|
||||
id: string;
|
||||
private mapSpace(space: {
|
||||
id: string | number;
|
||||
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,
|
||||
}): KintoneSpace {
|
||||
return {
|
||||
id: String(space.id),
|
||||
name: space.name,
|
||||
code: space.code,
|
||||
createdAt: space.createdAt,
|
||||
creator: space.creator,
|
||||
}));
|
||||
creator: space.creator
|
||||
? { code: space.creator.code, name: space.creator.name }
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== App APIs ====================
|
||||
|
||||
/**
|
||||
* Get list of apps, optionally filtered by space
|
||||
* GET /k/v1/apps.json?space={spaceId}
|
||||
*/
|
||||
async getApps(spaceId?: string): Promise<KintoneApp[]> {
|
||||
interface AppsResponse {
|
||||
apps: Array<{
|
||||
appId: string;
|
||||
private mapApp(app: {
|
||||
appId: string | number;
|
||||
name: string;
|
||||
code?: string;
|
||||
spaceId?: string;
|
||||
spaceId?: string | number;
|
||||
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,
|
||||
}): KintoneApp {
|
||||
return {
|
||||
appId: String(app.appId),
|
||||
name: app.name,
|
||||
code: app.code,
|
||||
spaceId: app.spaceId,
|
||||
spaceId: app.spaceId ? String(app.spaceId) : undefined,
|
||||
createdAt: app.createdAt,
|
||||
creator: app.creator,
|
||||
modifiedAt: app.modifiedAt,
|
||||
modifier: app.modifier,
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
private mapResource(resource: {
|
||||
type: string;
|
||||
file?: { fileKey: string; name: string };
|
||||
url?: string;
|
||||
}): JSFileConfig | CSSFileConfig {
|
||||
return {
|
||||
type: resource.type as "FILE" | "URL",
|
||||
file: resource.file
|
||||
? { fileKey: resource.file.fileKey, name: resource.file.name, size: 0 }
|
||||
: undefined,
|
||||
url: resource.url,
|
||||
};
|
||||
}
|
||||
|
||||
private buildCustomizeSection(
|
||||
js?: JSFileConfig[],
|
||||
css?: CSSFileConfig[],
|
||||
):
|
||||
| { js?: CustomizeResourceItem[]; css?: CustomizeResourceItem[] }
|
||||
| undefined {
|
||||
if (!js && !css) return undefined;
|
||||
|
||||
return {
|
||||
js: js?.map((item) => ({
|
||||
type: item.type,
|
||||
file: item.file ? { fileKey: item.file.fileKey } : undefined,
|
||||
url: item.url,
|
||||
})),
|
||||
css: css?.map((item) => ({
|
||||
type: item.type,
|
||||
file: item.file ? { fileKey: item.file.fileKey } : undefined,
|
||||
url: item.url,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 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));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app details including customization config
|
||||
* GET /k/v1/app.json?app={appId}
|
||||
*/
|
||||
async getAppDetail(appId: string): Promise<AppDetail> {
|
||||
interface AppResponse {
|
||||
appId: string;
|
||||
name: string;
|
||||
code?: string;
|
||||
description?: string;
|
||||
spaceId?: string;
|
||||
createdAt: string;
|
||||
creator: { code: string; name: string };
|
||||
modifiedAt: string;
|
||||
modifier: { code: string; name: string };
|
||||
}
|
||||
return this.withErrorHandling(async () => {
|
||||
const [appInfo, customizeInfo] = await Promise.all([
|
||||
this.client.app.getApp({ id: appId }),
|
||||
this.client.app.getAppCustomize({ app: appId }),
|
||||
]);
|
||||
|
||||
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,
|
||||
})) || [],
|
||||
customizeInfo.desktop?.js?.map((js) => this.mapResource(js)) || [],
|
||||
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,
|
||||
})) || [],
|
||||
customizeInfo.mobile?.js?.map((js) => this.mapResource(js)) || [],
|
||||
},
|
||||
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,
|
||||
})) || [],
|
||||
customizeInfo.desktop?.css?.map((css) => this.mapResource(css)) ||
|
||||
[],
|
||||
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,
|
||||
})) || [],
|
||||
customizeInfo.mobile?.css?.map((css) => this.mapResource(css)) ||
|
||||
[],
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
return {
|
||||
appId: appInfo.appId,
|
||||
appId: String(appInfo.appId),
|
||||
name: appInfo.name,
|
||||
code: appInfo.code,
|
||||
description: appInfo.description,
|
||||
spaceId: appInfo.spaceId,
|
||||
spaceId: appInfo.spaceId ? String(appInfo.spaceId) : undefined,
|
||||
createdAt: appInfo.createdAt,
|
||||
creator: appInfo.creator,
|
||||
modifiedAt: appInfo.modifiedAt,
|
||||
modifier: appInfo.modifier,
|
||||
customization,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== File APIs ====================
|
||||
|
||||
/**
|
||||
* Get file content from Kintone
|
||||
* GET /k/v1/file.json?fileKey={fileKey}
|
||||
*/
|
||||
async getFileContent(fileKey: string): Promise<FileContent> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
|
||||
|
||||
try {
|
||||
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 this.withErrorHandling(async () => {
|
||||
const data = await this.client.file.downloadFile({ fileKey });
|
||||
const buffer = Buffer.from(data);
|
||||
const content = buffer.toString("base64");
|
||||
|
||||
return {
|
||||
fileKey,
|
||||
name: fileKey, // Kintone doesn't return filename in file API
|
||||
size: arrayBuffer.byteLength,
|
||||
mimeType: contentType,
|
||||
name: fileKey,
|
||||
size: buffer.byteLength,
|
||||
mimeType: "application/octet-stream",
|
||||
content,
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to Kintone
|
||||
* POST /k/v1/file.json (multipart/form-data)
|
||||
*/
|
||||
async uploadFile(
|
||||
content: string | Buffer,
|
||||
fileName: string,
|
||||
mimeType: string = "application/javascript",
|
||||
_mimeType?: string,
|
||||
): Promise<{ fileKey: string }> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
|
||||
|
||||
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 this.withErrorHandling(async () => {
|
||||
const response = await this.client.file.uploadFile({
|
||||
file: { name: fileName, data: content },
|
||||
});
|
||||
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 ====================
|
||||
|
||||
/**
|
||||
* Update app customization config
|
||||
* PUT /k/v1/app/customize.json
|
||||
*/
|
||||
async updateAppCustomize(
|
||||
appId: string,
|
||||
config: AppCustomizationConfig,
|
||||
): Promise<void> {
|
||||
interface CustomizeUpdateRequest {
|
||||
app: string;
|
||||
desktop?: {
|
||||
js?: Array<{ type: string; file?: { fileKey: string }; url?: string }>;
|
||||
css?: Array<{ type: string; file?: { fileKey: string }; url?: string }>;
|
||||
};
|
||||
mobile?: {
|
||||
js?: Array<{ type: string; file?: { fileKey: string }; url?: string }>;
|
||||
css?: Array<{ type: string; file?: { fileKey: string }; url?: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
const requestBody: CustomizeUpdateRequest = {
|
||||
return this.withErrorHandling(async () => {
|
||||
await this.client.app.updateAppCustomize({
|
||||
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),
|
||||
desktop: this.buildCustomizeSection(
|
||||
config.javascript?.pc,
|
||||
config.stylesheet?.pc,
|
||||
),
|
||||
mobile: this.buildCustomizeSection(
|
||||
config.javascript?.mobile,
|
||||
config.stylesheet?.mobile,
|
||||
),
|
||||
scope: "ALL",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy app changes
|
||||
* POST /k/v1/preview/app/deploy.json
|
||||
*/
|
||||
async deployApp(appId: string): Promise<void> {
|
||||
await this.request("preview/app/deploy.json", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ apps: [{ app: appId }] }),
|
||||
return this.withErrorHandling(async () => {
|
||||
await this.client.app.deployApp({ apps: [{ app: appId }] });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get deploy status
|
||||
* GET /k/v1/preview/app/deploy.json?app={appId}
|
||||
*/
|
||||
async getDeployStatus(
|
||||
appId: string,
|
||||
): Promise<"PROCESSING" | "SUCCESS" | "FAIL" | "CANCEL"> {
|
||||
interface DeployStatusResponse {
|
||||
apps: Array<{
|
||||
app: string;
|
||||
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";
|
||||
return this.withErrorHandling(async () => {
|
||||
const response = await this.client.app.getDeployStatus({ app: appId });
|
||||
return response.apps[0]?.status || "FAIL";
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Utility Methods ====================
|
||||
|
||||
/**
|
||||
* Test connection to Kintone
|
||||
*/
|
||||
async testConnection(): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
await this.getApps();
|
||||
@@ -494,15 +300,17 @@ export class KintoneClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the domain name
|
||||
*/
|
||||
getDomain(): string {
|
||||
return this.domain;
|
||||
}
|
||||
}
|
||||
|
||||
// Export factory function for convenience
|
||||
type CustomizeResourceItem = {
|
||||
type: string;
|
||||
file?: { fileKey: string };
|
||||
url?: string;
|
||||
};
|
||||
|
||||
export function createKintoneClient(domain: DomainWithPassword): KintoneClient {
|
||||
return new KintoneClient(domain);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user