diff --git a/package.json b/package.json index 2265055..8cbebbe 100644 --- a/package.json +++ b/package.json @@ -26,12 +26,15 @@ "devDependencies": { "@kevisual/dts": "^0.0.3", "@kevisual/types": "^0.0.10", - "@kevisual/use-config": "^1.0.19", - "@types/bun": "^1.3.2", + "@kevisual/use-config": "^1.0.21", + "@types/bun": "^1.3.3", "@types/node": "^24.10.1", "nocodb-sdk": "^0.265.1" }, "exports": { ".": "./dist/app.js" + }, + "dependencies": { + "form-data": "^4.0.5" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59e96e7..1962eb4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + form-data: + specifier: ^4.0.5 + version: 4.0.5 devDependencies: '@kevisual/dts': specifier: ^0.0.3 @@ -15,11 +19,11 @@ importers: specifier: ^0.0.10 version: 0.0.10 '@kevisual/use-config': - specifier: ^1.0.19 - version: 1.0.19(dotenv@16.6.1) + specifier: ^1.0.21 + version: 1.0.21(dotenv@16.6.1) '@types/bun': - specifier: ^1.3.2 - version: 1.3.2(@types/react@19.2.6) + specifier: ^1.3.3 + version: 1.3.3 '@types/node': specifier: ^24.10.1 version: 24.10.1 @@ -62,10 +66,10 @@ packages: '@kevisual/types@0.0.10': resolution: {integrity: sha512-Q73uzzjk9UidumnmCvOpgzqDDvQxsblz22bIFuoiioUFJWwaparx8bpd8ArRyFojicYL1YJoFDzDZ9j9NN8grA==} - '@kevisual/use-config@1.0.19': - resolution: {integrity: sha512-Q1IH4eMqUe5w6Bq8etoqOSls9FPIy0xwwD3wHf26EsQLZadhccI9qkDuFzP/rFWDa57mwFPEfwbGE5UlqWOCkw==} + '@kevisual/use-config@1.0.21': + resolution: {integrity: sha512-czgy4+tBDBJI6QTnKh2PCwswET6ZpZ4ZqBE/SPkkOivEtlrcPzLs5elwMLZ3goD1XMD4VB3yjumb5WuW/8H8MA==} peerDependencies: - dotenv: ^16.4.7 + dotenv: ^17 '@rollup/plugin-commonjs@28.0.9': resolution: {integrity: sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==} @@ -217,8 +221,8 @@ packages: cpu: [x64] os: [win32] - '@types/bun@1.3.2': - resolution: {integrity: sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg==} + '@types/bun@1.3.3': + resolution: {integrity: sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -226,9 +230,6 @@ packages: '@types/node@24.10.1': resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} - '@types/react@19.2.6': - resolution: {integrity: sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==} - '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -238,10 +239,8 @@ packages: axios@1.11.0: resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} - bun-types@1.3.2: - resolution: {integrity: sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg==} - peerDependencies: - '@types/react': ^19 + bun-types@1.3.3: + resolution: {integrity: sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ==} call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} @@ -257,9 +256,6 @@ packages: commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} @@ -319,8 +315,8 @@ packages: debug: optional: true - form-data@4.0.4: - resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} fsevents@2.3.3: @@ -492,7 +488,7 @@ snapshots: '@kevisual/types@0.0.10': {} - '@kevisual/use-config@1.0.19(dotenv@16.6.1)': + '@kevisual/use-config@1.0.21(dotenv@16.6.1)': dependencies: '@kevisual/load': 0.0.6 dotenv: 16.6.1 @@ -602,11 +598,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.53.3': optional: true - '@types/bun@1.3.2(@types/react@19.2.6)': + '@types/bun@1.3.3': dependencies: - bun-types: 1.3.2(@types/react@19.2.6) - transitivePeerDependencies: - - '@types/react' + bun-types: 1.3.3 '@types/estree@1.0.8': {} @@ -614,10 +608,6 @@ snapshots: dependencies: undici-types: 7.16.0 - '@types/react@19.2.6': - dependencies: - csstype: 3.2.3 - '@types/resolve@1.20.2': {} asynckit@0.4.0: {} @@ -625,15 +615,14 @@ snapshots: axios@1.11.0: dependencies: follow-redirects: 1.15.11 - form-data: 4.0.4 + form-data: 4.0.5 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - bun-types@1.3.2(@types/react@19.2.6): + bun-types@1.3.3: dependencies: '@types/node': 24.10.1 - '@types/react': 19.2.6 call-bind-apply-helpers@1.0.2: dependencies: @@ -655,8 +644,6 @@ snapshots: commondir@1.0.1: {} - csstype@3.2.3: {} - dayjs@1.11.13: {} deepmerge@4.3.1: {} @@ -696,7 +683,7 @@ snapshots: follow-redirects@1.15.11: {} - form-data@4.0.4: + form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 diff --git a/public/mole.png b/public/mole.png new file mode 100644 index 0000000..25a341b Binary files /dev/null and b/public/mole.png differ diff --git a/src/api.ts b/src/api.ts index 49b5e0a..644cad5 100644 --- a/src/api.ts +++ b/src/api.ts @@ -13,6 +13,9 @@ type MakeRequestOptions = { data?: Record; method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; json?: boolean; + // body 优先级别高于 data + isFromData?: boolean; + body?: any; }; export class Query { baseURL: string; @@ -24,6 +27,7 @@ export class Query { makeRequest(endpoint: string, options: MakeRequestOptions) { const url = new URL(endpoint, this.baseURL); const isJson = options.json ?? true; + const bodyIsFormData = options.isFromData || false; if (options.params) { Object.entries(options.params).forEach(([key, value]) => { url.searchParams.append(key, String(value)); @@ -33,12 +37,19 @@ export class Query { const headers: HeadersInit = { 'xc-token': `${this.token}`, }; - headers['Content-Type'] = 'application/json'; + + // 如果 body 不是 FormData,才设置 Content-Type + if (!bodyIsFormData) { + headers['Content-Type'] = 'application/json'; + } + const fetchOptions: FetchOptions = { method, headers, }; - if (options.data) { + if (options.body) { + fetchOptions.body = options.body; + } else if (options.data) { fetchOptions.body = JSON.stringify(options.data); } return fetch(url.href, fetchOptions).then(async (response) => { @@ -47,6 +58,13 @@ export class Query { } if (isJson) { const result = await response.json(); + const isArray = Array.isArray(result); + if (isArray) { + return { + code: 200, + list: result, + } as ResponseList; + } result.code = 200; return result; } diff --git a/src/data/upload.ts b/src/data/upload.ts new file mode 100644 index 0000000..4a17206 --- /dev/null +++ b/src/data/upload.ts @@ -0,0 +1,79 @@ + +import { Query } from '../api.ts'; +export class Upload { + private query: Query; + + constructor(query: Query) { + this.query = query; + } + /** + * 创建上传, 上传后,自动返回的数据是列表 + * @param opts + * @returns + */ + public async createUpload(opts: UploadOpts): Promise<{ + code: number; + list: UploadItem[]; + }> { + const formData = new FormData(); + const { path, mimeType, file, size, title, url } = opts; + if (url) { + formData.append('url', url); + } + if (file) { + formData.append('file', file, title); + } + if (path) { + formData.append('path', path); + } + if (mimeType) { + formData.append('mimetype', mimeType); + } + if (size) { + formData.append('size', size.toString()); + } + if (title) { + formData.append('title', title); + } + + + return await this.query.makeRequest('/api/v2/storage/upload', { + method: 'POST', + body: formData, + isFromData: true, + }); + } +} + +type UploadOpts = { + path?: string; + mimeType?: string; + file: any; + size?: number; + title: string; + url?: string; // 可选,互联网资源链接 +} +type UploadItem = { + /** + * nocodb 资源路径 + */ + path: string; + /** + * 文件标题 + */ + title: string; + /** + * 文件大小,单位字节 + */ + size: number; + /** + * 文件 MIME 类型 + */ + mimetype: string; + width: number; + height: number; + /** + * 签名后的完整访问路径 + */ + signedPath: string; +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index a9645d3..5036138 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,4 @@ export * from './api.ts'; export * from './main.ts'; export * from './meta/index.ts'; +export * from './data/upload.ts'; \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 53b29af..1201fc9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,7 @@ import { Query } from './api.ts'; import { Meta } from './meta/index.ts'; import { Record } from './record.ts'; +import { Upload } from './data/upload.ts'; export type NocoApiOptions = { table?: string; token?: string; @@ -11,6 +12,7 @@ export class NocoApi { query: Query; record: Record; meta: Meta; + upload: Upload; constructor(options?: NocoApiOptions) { const table = options?.table; @@ -19,6 +21,7 @@ export class NocoApi { this.query = new Query({ baseURL, token }); this.record = new Record(this.query, table); this.meta = new Meta({ query: this.query }); + this.upload = new Upload(this.query); } /** * diff --git a/src/utils/mime-type.ts b/src/utils/mime-type.ts new file mode 100644 index 0000000..8cb083c --- /dev/null +++ b/src/utils/mime-type.ts @@ -0,0 +1,58 @@ +export const extname = (str: string): string => { + const match = str.match(/\.[^\.]+$/); + return match ? match[0] : ''; +} +/** + * 根据文件扩展名获取 MIME 类型 + * @param fileName - 文件名 + * @returns MIME 类型 + */ +export function getMimeType(fileName: string): string { + const ext = extname(fileName).toLowerCase(); + const mimeTypes: Record = { + // 图片 + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml', + '.bmp': 'image/bmp', + '.ico': 'image/x-icon', + + // 文档 + '.pdf': 'application/pdf', + '.doc': 'application/msword', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.xls': 'application/vnd.ms-excel', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.ppt': 'application/vnd.ms-powerpoint', + '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + '.txt': 'text/plain', + '.csv': 'text/csv', + + // 音视频 + '.mp3': 'audio/mpeg', + '.mp4': 'video/mp4', + '.wav': 'audio/wav', + '.avi': 'video/x-msvideo', + '.mov': 'video/quicktime', + + // 压缩文件 + '.zip': 'application/zip', + '.rar': 'application/x-rar-compressed', + '.7z': 'application/x-7z-compressed', + '.tar': 'application/x-tar', + '.gz': 'application/gzip', + + // 代码文件 + '.json': 'application/json', + '.xml': 'application/xml', + '.html': 'text/html', + '.css': 'text/css', + '.js': 'text/javascript', + '.ts': 'text/typescript', + }; + + return mimeTypes[ext] || 'application/octet-stream'; +} \ No newline at end of file diff --git a/test/common.ts b/test/common.ts index f249b09..adca2c8 100644 --- a/test/common.ts +++ b/test/common.ts @@ -1,9 +1,11 @@ import { NocoApi } from './../src/main.ts'; import { useConfig } from '@kevisual/use-config' export const config = useConfig() - +import util from 'node:util'; // # 签到表 -const table = 'mcby44q8zrayvn9' +// const table = 'mcby44q8zrayvn9' +// 本地 +const table = 'mi89er1m951pb3g' export const nocoApi = new NocoApi({ baseURL: config.NOCODB_URL || 'http://localhost:8080', token: config.NOCODB_API_KEY || '', @@ -11,4 +13,5 @@ export const nocoApi = new NocoApi({ }); -// console.log('nocoApi', await nocoApi.record.list()) \ No newline at end of file +// const list = await nocoApi.record.list() +// console.log(util.inspect(list, { depth: null, colors: true })) diff --git a/test/upload.ts b/test/upload.ts new file mode 100644 index 0000000..1591333 --- /dev/null +++ b/test/upload.ts @@ -0,0 +1,21 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { nocoApi } from './common'; +import util from 'node:util'; +const filePath = path.resolve(process.cwd(), 'public/mole.png'); +const absolutePath = path.resolve(process.cwd(), filePath); +const fileBuffer = fs.readFileSync(absolutePath); +const blob = new Blob([fileBuffer], { type: 'image/png' }); + +const res = await nocoApi.upload.createUpload({ + file: blob, + title: 'mole3.png', +}) +console.log('上传结果:'); +console.log(util.inspect(res, { depth: null })); +// const update = await nocoApi.record.update({ +// Id: 4, +// Picture: res.list +// }) + +// console.log(update) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index a25f1d0..7753091 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "@kevisual/types/json/backend.json", "compilerOptions": { + "module": "NodeNext", + "target": "esnext", "baseUrl": ".", "typeRoots": [ "./node_modules/@types",