@modesto_ma/dsh-channels 0.5.0-beta.1 → 0.5.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Feishu wiki (知识库) and docx OpenAPI calls used by the wiki tools, made
3
+ * with the app identity (tenant access token).
4
+ *
5
+ * Structure calls use `wiki/v2` (`spaces/get_node`, `spaces/:id`,
6
+ * `spaces/:id/nodes`); document content uses the `docs_ai/v1` document
7
+ * endpoints, which read and write Markdown or Feishu document XML directly
8
+ * (block ids are exported only in XML); the revision check uses
9
+ * `docx/v1/documents/:id`. Every call maps a thrown transport error or a
10
+ * non-zero Feishu `code` to {@link FeishuApiError}.
11
+ *
12
+ * @module dsh-channels/feishu/wiki/api
13
+ */
14
+
15
+ import { FeishuApiError } from '../../feishu-channel.ts'
16
+
17
+ /** Object kinds a wiki node can hold. */
18
+ export type WikiObjType = 'doc' | 'docx' | 'sheet' | 'mindnote' | 'bitable' | 'file' | 'slides'
19
+
20
+ /** One wiki node as the tools use it. */
21
+ export interface WikiNode {
22
+ readonly spaceId: string
23
+ readonly nodeToken: string
24
+ /** Token of the document the node holds (the docx `document_id` for docx). */
25
+ readonly objToken: string
26
+ readonly objType: WikiObjType
27
+ /** Parent node; absent for a top-level node of its space. */
28
+ readonly parentNodeToken?: string
29
+ readonly nodeType: 'origin' | 'shortcut'
30
+ readonly title: string
31
+ readonly hasChild: boolean
32
+ /** Owner id as Feishu reports it (an open_id; the app's bot open_id when the app owns it). */
33
+ readonly owner?: string
34
+ /** Web link, when Feishu returned one. */
35
+ readonly url?: string
36
+ }
37
+
38
+ /** One page of child nodes. */
39
+ export interface WikiNodePage {
40
+ readonly items: readonly WikiNode[]
41
+ /** Cursor of the next page; absent on the last page. */
42
+ readonly pageToken?: string
43
+ }
44
+
45
+ /** Read scopes of `docs_ai` fetch. */
46
+ export type WikiReadScope = 'full' | 'outline' | 'section' | 'range' | 'keyword'
47
+
48
+ /** Content format of `docs_ai` reads and writes. */
49
+ export type WikiDocFormat = 'markdown' | 'xml'
50
+
51
+ /** One `docs_ai` fetch request. */
52
+ export interface WikiFetchRequest {
53
+ readonly format: WikiDocFormat
54
+ /** Export block ids (XML only; Feishu ignores it for Markdown). */
55
+ readonly withBlockIds: boolean
56
+ readonly scope: WikiReadScope
57
+ readonly startBlockId?: string
58
+ readonly endBlockId?: string
59
+ readonly keyword?: string
60
+ /** `outline`: heading level cap; other scopes: subtree depth. */
61
+ readonly maxDepth?: number
62
+ }
63
+
64
+ /** One fetched document. */
65
+ export interface WikiDocument {
66
+ readonly revisionId: number
67
+ readonly content: string
68
+ }
69
+
70
+ /** `docs_ai` update commands. */
71
+ export type WikiUpdateCommand =
72
+ | 'str_replace'
73
+ | 'block_insert_after'
74
+ | 'block_replace'
75
+ | 'block_delete'
76
+ | 'overwrite'
77
+
78
+ /** One `docs_ai` update request. */
79
+ export interface WikiUpdateRequest {
80
+ readonly command: WikiUpdateCommand
81
+ readonly format: WikiDocFormat
82
+ readonly content?: string
83
+ readonly pattern?: string
84
+ /** Anchor or target block; `-1` = document end for `block_insert_after`. */
85
+ readonly blockId?: string
86
+ readonly startBlockId?: string
87
+ readonly endBlockId?: string
88
+ /** Base revision; Feishu rejects the update when the document moved past it. */
89
+ readonly revisionId?: number
90
+ }
91
+
92
+ /** Outcome of one update. */
93
+ export interface WikiUpdateResult {
94
+ /** `success`, `partial_success`, or `failed` as Feishu reports it. */
95
+ readonly result: string
96
+ readonly revisionId?: number
97
+ readonly updatedBlocks?: number
98
+ readonly warnings: readonly string[]
99
+ }
100
+
101
+ /** The wiki/docx endpoints the wiki tools consume. */
102
+ export interface WikiApi {
103
+ /**
104
+ * Resolve a node token (or, with `objType: 'docx'`, a docx token mounted in
105
+ * the wiki) to its node.
106
+ * @param token Node token or docx token.
107
+ * @param objType `wiki` for a node token, `docx` for a document token.
108
+ * @returns The node.
109
+ */
110
+ getNode(token: string, objType: 'wiki' | 'docx'): Promise<WikiNode>
111
+ /**
112
+ * Name of one space.
113
+ * @param spaceId Wiki space id.
114
+ * @returns The space name.
115
+ */
116
+ spaceName(spaceId: string): Promise<string>
117
+ /**
118
+ * One page of children; the space's top-level nodes without a parent.
119
+ * @param spaceId Wiki space id.
120
+ * @param parentNodeToken Parent node, or undefined for the space top level.
121
+ * @param pageToken Cursor from the previous page.
122
+ * @returns The page.
123
+ */
124
+ listChildren(spaceId: string, parentNodeToken: string | undefined, pageToken?: string): Promise<WikiNodePage>
125
+ /**
126
+ * Create one empty docx, spreadsheet, or Base node.
127
+ * @param spaceId Wiki space id.
128
+ * @param parentNodeToken Parent node, or undefined for the space top level.
129
+ * @param title Node title.
130
+ * @param objType Object kind of the new node.
131
+ * @returns The new node.
132
+ */
133
+ createNode(spaceId: string, parentNodeToken: string | undefined, title: string, objType: 'docx' | 'sheet' | 'bitable'): Promise<WikiNode>
134
+ /**
135
+ * Current revision of one docx document.
136
+ * @param documentId Docx document id (the node's `objToken`).
137
+ * @returns The revision id.
138
+ */
139
+ documentRevision(documentId: string): Promise<number>
140
+ /**
141
+ * Read one docx document.
142
+ * @param documentId Docx document id.
143
+ * @param request Format and scope.
144
+ * @returns Content and revision.
145
+ */
146
+ fetchDocument(documentId: string, request: WikiFetchRequest): Promise<WikiDocument>
147
+ /**
148
+ * Apply one update command to a docx document.
149
+ * @param documentId Docx document id.
150
+ * @param request The command.
151
+ * @returns Feishu's outcome.
152
+ */
153
+ updateDocument(documentId: string, request: WikiUpdateRequest): Promise<WikiUpdateResult>
154
+ }
155
+
156
+ /** The SDK's generic authenticated request (`Client.request`). */
157
+ export type WikiRawRequest = <T>(input: {
158
+ method: 'GET' | 'POST' | 'PUT'
159
+ url: string
160
+ params?: Record<string, string | number>
161
+ data?: object
162
+ }) => Promise<T>
163
+
164
+ interface FeishuEnvelope<T> {
165
+ code?: number
166
+ msg?: string
167
+ data?: T
168
+ }
169
+
170
+ interface RawNode {
171
+ space_id?: string
172
+ node_token?: string
173
+ obj_token?: string
174
+ obj_type?: string
175
+ parent_node_token?: string
176
+ node_type?: string
177
+ title?: string
178
+ has_child?: boolean
179
+ owner?: string
180
+ url?: string
181
+ }
182
+
183
+ const OBJ_TYPES: ReadonlySet<string> = new Set(['doc', 'docx', 'sheet', 'mindnote', 'bitable', 'file', 'slides'])
184
+
185
+ /** Feishu's per-page maximum for `spaces/:id/nodes`. */
186
+ const LIST_PAGE_SIZE = 50
187
+
188
+ function thrownError(error: unknown): FeishuApiError {
189
+ if (error instanceof FeishuApiError) return error
190
+ const data = (error as { response?: { data?: { code?: unknown; msg?: unknown } } } | undefined)?.response?.data
191
+ const code = typeof data?.code === 'number' ? data.code : undefined
192
+ const msg = typeof data?.msg === 'string' ? data.msg : undefined
193
+ if (code === undefined && msg === undefined) return new FeishuApiError(error instanceof Error ? error.message : String(error))
194
+ return new FeishuApiError([code === undefined ? undefined : `code=${code}`, msg].filter(Boolean).join(' '), code)
195
+ }
196
+
197
+ /**
198
+ * One Feishu OpenAPI call: unwraps `data` and maps a transport error or a
199
+ * non-zero `code` to {@link FeishuApiError}.
200
+ *
201
+ * @param request The SDK's generic request.
202
+ * @param input Method, URL, query, and body.
203
+ * @returns The response `data`.
204
+ */
205
+ export async function call<T>(request: WikiRawRequest, input: Parameters<WikiRawRequest>[0]): Promise<T> {
206
+ let response: FeishuEnvelope<T>
207
+ try {
208
+ response = await request<FeishuEnvelope<T>>(input)
209
+ } catch (error) {
210
+ throw thrownError(error)
211
+ }
212
+ if (response.code !== undefined && response.code !== 0) {
213
+ throw new FeishuApiError(`code=${response.code}${response.msg === undefined ? '' : ` ${response.msg}`}`, response.code)
214
+ }
215
+ if (response.data === undefined) throw new FeishuApiError(`${input.url}: response has no data`)
216
+ return response.data
217
+ }
218
+
219
+ function wikiNode(raw: RawNode | undefined, where: string): WikiNode {
220
+ if (raw?.space_id === undefined || raw.node_token === undefined || raw.obj_token === undefined
221
+ || raw.obj_type === undefined || !OBJ_TYPES.has(raw.obj_type)) {
222
+ throw new FeishuApiError(`${where}: response node is incomplete`)
223
+ }
224
+ return {
225
+ spaceId: raw.space_id,
226
+ nodeToken: raw.node_token,
227
+ objToken: raw.obj_token,
228
+ objType: raw.obj_type as WikiObjType,
229
+ ...(raw.parent_node_token === undefined || raw.parent_node_token === '' ? {} : { parentNodeToken: raw.parent_node_token }),
230
+ nodeType: raw.node_type === 'shortcut' ? 'shortcut' : 'origin',
231
+ title: raw.title ?? '',
232
+ hasChild: raw.has_child === true,
233
+ ...(raw.owner === undefined || raw.owner === '' ? {} : { owner: raw.owner }),
234
+ ...(raw.url === undefined || raw.url === '' ? {} : { url: raw.url }),
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Encode one URL path segment.
240
+ *
241
+ * @param value Raw segment.
242
+ * @returns The encoded segment.
243
+ */
244
+ export function segment(value: string): string {
245
+ return encodeURIComponent(value)
246
+ }
247
+
248
+ /**
249
+ * {@link WikiApi} over the SDK's generic authenticated request.
250
+ *
251
+ * @param request `Client.request` of the connected SDK client.
252
+ * @returns The API.
253
+ */
254
+ export function createWikiApi(request: WikiRawRequest): WikiApi {
255
+ return {
256
+ async getNode(token, objType) {
257
+ const url = '/open-apis/wiki/v2/spaces/get_node'
258
+ const data = await call<{ node?: RawNode }>(request, { method: 'GET', url, params: { token, obj_type: objType } })
259
+ return wikiNode(data.node, url)
260
+ },
261
+ async spaceName(spaceId) {
262
+ const data = await call<{ space?: { name?: string } }>(request, {
263
+ method: 'GET', url: `/open-apis/wiki/v2/spaces/${segment(spaceId)}`,
264
+ })
265
+ return data.space?.name ?? ''
266
+ },
267
+ async listChildren(spaceId, parentNodeToken, pageToken) {
268
+ const url = `/open-apis/wiki/v2/spaces/${segment(spaceId)}/nodes`
269
+ const data = await call<{ items?: RawNode[]; page_token?: string; has_more?: boolean }>(request, {
270
+ method: 'GET',
271
+ url,
272
+ params: {
273
+ page_size: LIST_PAGE_SIZE,
274
+ ...(parentNodeToken === undefined ? {} : { parent_node_token: parentNodeToken }),
275
+ ...(pageToken === undefined ? {} : { page_token: pageToken }),
276
+ },
277
+ })
278
+ return {
279
+ items: (data.items ?? []).map(item => wikiNode(item, url)),
280
+ ...(data.has_more === true && data.page_token !== undefined && data.page_token !== '' ? { pageToken: data.page_token } : {}),
281
+ }
282
+ },
283
+ async createNode(spaceId, parentNodeToken, title, objType) {
284
+ const url = `/open-apis/wiki/v2/spaces/${segment(spaceId)}/nodes`
285
+ const data = await call<{ node?: RawNode }>(request, {
286
+ method: 'POST',
287
+ url,
288
+ data: {
289
+ obj_type: objType,
290
+ node_type: 'origin',
291
+ title,
292
+ ...(parentNodeToken === undefined ? {} : { parent_node_token: parentNodeToken }),
293
+ },
294
+ })
295
+ return wikiNode(data.node, url)
296
+ },
297
+ async documentRevision(documentId) {
298
+ const url = `/open-apis/docx/v1/documents/${segment(documentId)}`
299
+ const data = await call<{ document?: { revision_id?: number } }>(request, { method: 'GET', url })
300
+ const revision = data.document?.revision_id
301
+ if (typeof revision !== 'number') throw new FeishuApiError(`${url}: response has no revision_id`)
302
+ return revision
303
+ },
304
+ async fetchDocument(documentId, fetch) {
305
+ const url = `/open-apis/docs_ai/v1/documents/${segment(documentId)}/fetch`
306
+ const ids = fetch.format === 'xml' && fetch.withBlockIds
307
+ const readOption = fetch.scope === 'full' ? undefined : {
308
+ read_mode: fetch.scope,
309
+ ...(fetch.startBlockId === undefined ? {} : { start_block_id: fetch.startBlockId }),
310
+ ...(fetch.endBlockId === undefined ? {} : { end_block_id: fetch.endBlockId }),
311
+ ...(fetch.keyword === undefined ? {} : { keyword: fetch.keyword }),
312
+ ...(fetch.maxDepth === undefined ? {} : { max_depth: String(fetch.maxDepth) }),
313
+ }
314
+ const data = await call<{ document?: { revision_id?: number; content?: string } }>(request, {
315
+ method: 'POST',
316
+ url,
317
+ data: {
318
+ format: fetch.format,
319
+ export_option: { export_block_id: ids, export_style_attrs: false, export_cite_extra_data: false },
320
+ ...(readOption === undefined ? {} : { read_option: readOption }),
321
+ },
322
+ })
323
+ const document = data.document
324
+ if (document === undefined || typeof document.content !== 'string') {
325
+ throw new FeishuApiError(`${url}: response has no document content`)
326
+ }
327
+ return { revisionId: document.revision_id ?? 0, content: document.content }
328
+ },
329
+ async updateDocument(documentId, update) {
330
+ const url = `/open-apis/docs_ai/v1/documents/${segment(documentId)}`
331
+ const data = await call<{
332
+ result?: string
333
+ updated_blocks_count?: number
334
+ warnings?: unknown
335
+ document?: { revision_id?: number }
336
+ }>(request, {
337
+ method: 'PUT',
338
+ url,
339
+ data: {
340
+ format: update.format,
341
+ command: update.command,
342
+ ...(update.content === undefined ? {} : { content: update.content }),
343
+ ...(update.pattern === undefined ? {} : { pattern: update.pattern }),
344
+ ...(update.blockId === undefined ? {} : { block_id: update.blockId }),
345
+ ...(update.startBlockId === undefined ? {} : { start_block_id: update.startBlockId }),
346
+ ...(update.endBlockId === undefined ? {} : { end_block_id: update.endBlockId }),
347
+ ...(update.revisionId === undefined ? {} : { revision_id: update.revisionId }),
348
+ },
349
+ })
350
+ const warnings = Array.isArray(data.warnings)
351
+ ? data.warnings.filter((item): item is string => typeof item === 'string' && item !== '')
352
+ : []
353
+ return {
354
+ result: data.result ?? 'success',
355
+ ...(data.document?.revision_id === undefined ? {} : { revisionId: data.document.revision_id }),
356
+ ...(data.updated_blocks_count === undefined ? {} : { updatedBlocks: data.updated_blocks_count }),
357
+ warnings,
358
+ }
359
+ },
360
+ }
361
+ }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Feishu Base (多维表格) calls used by the wiki base tools, made with the app
3
+ * identity (tenant access token).
4
+ *
5
+ * Everything goes through `base/v3/bases/:base_token/...` (the legacy
6
+ * `bitable/v1` API is not used). Record reads return a column matrix that
7
+ * this module turns into one field map per record; record writes take flat
8
+ * maps keyed by field name or field id. Writes carry no precondition, are
9
+ * applied asynchronously (a read straight after a write may not show it), and
10
+ * must not run in parallel on one table.
11
+ *
12
+ * @module dsh-channels/feishu/wiki/base-api
13
+ */
14
+
15
+ import { FeishuApiError } from '../../feishu-channel.ts'
16
+ import { call, segment } from './api.ts'
17
+ import type { WikiRawRequest } from './api.ts'
18
+
19
+ /** Most records one batch write may carry (Feishu limit). */
20
+ export const BASE_BATCH_MAX_RECORDS = 200
21
+
22
+ /** One table of a base. */
23
+ export interface BaseTable {
24
+ readonly tableId: string
25
+ readonly name: string
26
+ }
27
+
28
+ /** One field (column) of a table, as Feishu describes it. */
29
+ export interface BaseField {
30
+ readonly fieldId: string
31
+ readonly name: string
32
+ /** `text`, `number`, `select`, `datetime`, `user`, `link`, `checkbox`, … */
33
+ readonly type: string
34
+ /** Remaining Feishu field properties (style, options, multiple, …), passed through. */
35
+ readonly properties: Readonly<Record<string, unknown>>
36
+ }
37
+
38
+ /** One cell value as Feishu reads or writes it (JSON). */
39
+ export type BaseCellValue = unknown
40
+
41
+ /** One record: its id and a map from field name to value. */
42
+ export interface BaseRecord {
43
+ readonly recordId: string
44
+ readonly fields: Readonly<Record<string, BaseCellValue>>
45
+ }
46
+
47
+ /** Filter, sort, projection, and paging of a record query. */
48
+ export interface BaseRecordQuery {
49
+ /** `{logic, conditions: [[field, operator, value?], …]}`. */
50
+ readonly filter?: object
51
+ /** `[{field, desc}]`. */
52
+ readonly sort?: readonly object[]
53
+ /** Field names or ids to return; all view-visible fields when absent. */
54
+ readonly fields?: readonly string[]
55
+ readonly viewId?: string
56
+ readonly offset: number
57
+ readonly limit: number
58
+ }
59
+
60
+ /** A keyword search over some fields. */
61
+ export interface BaseRecordSearch extends BaseRecordQuery {
62
+ readonly keyword: string
63
+ readonly searchFields: readonly string[]
64
+ }
65
+
66
+ /** One page of records. */
67
+ export interface BaseRecordPage {
68
+ readonly records: readonly BaseRecord[]
69
+ readonly hasMore: boolean
70
+ /** Table data snapshot version; changes when the data changes. */
71
+ readonly rev?: number
72
+ readonly timezone?: string
73
+ /** Fields Feishu skipped, with its reason. */
74
+ readonly ignoredFields: readonly string[]
75
+ }
76
+
77
+ /** Field values of one record write, keyed by field name or id. */
78
+ export type BaseRecordFields = Readonly<Record<string, BaseCellValue>>
79
+
80
+ /** The `base/v3` endpoints the wiki base tools consume. */
81
+ export interface BaseApi {
82
+ /**
83
+ * Tables of one base.
84
+ * @param baseToken Base token (the wiki node's `objToken`).
85
+ * @returns The tables.
86
+ */
87
+ listTables(baseToken: string): Promise<readonly BaseTable[]>
88
+ /**
89
+ * Fields of one table.
90
+ * @param baseToken Base token.
91
+ * @param tableId Table id.
92
+ * @returns The fields.
93
+ */
94
+ listFields(baseToken: string, tableId: string): Promise<readonly BaseField[]>
95
+ /**
96
+ * One page of records, filtered and sorted.
97
+ * @param baseToken Base token.
98
+ * @param tableId Table id.
99
+ * @param query Filter, sort, projection, paging.
100
+ * @returns The page.
101
+ */
102
+ listRecords(baseToken: string, tableId: string, query: BaseRecordQuery): Promise<BaseRecordPage>
103
+ /**
104
+ * One page of records matching a keyword.
105
+ * @param baseToken Base token.
106
+ * @param tableId Table id.
107
+ * @param search Keyword, searched fields, and query options.
108
+ * @returns The page.
109
+ */
110
+ searchRecords(baseToken: string, tableId: string, search: BaseRecordSearch): Promise<BaseRecordPage>
111
+ /**
112
+ * Create records (at most {@link BASE_BATCH_MAX_RECORDS}).
113
+ * @param baseToken Base token.
114
+ * @param tableId Table id.
115
+ * @param records Field maps.
116
+ * @returns The new record ids, in input order.
117
+ */
118
+ createRecords(baseToken: string, tableId: string, records: readonly BaseRecordFields[]): Promise<readonly string[]>
119
+ /**
120
+ * Update records (at most {@link BASE_BATCH_MAX_RECORDS}); only the given fields change.
121
+ * @param baseToken Base token.
122
+ * @param tableId Table id.
123
+ * @param updates Record id → changed fields.
124
+ * @returns Fields Feishu ignored (read-only fields), with its reason.
125
+ */
126
+ updateRecords(baseToken: string, tableId: string, updates: Readonly<Record<string, BaseRecordFields>>): Promise<readonly string[]>
127
+ /**
128
+ * Delete records (at most {@link BASE_BATCH_MAX_RECORDS}).
129
+ * @param baseToken Base token.
130
+ * @param tableId Table id.
131
+ * @param recordIds Records to delete.
132
+ */
133
+ deleteRecords(baseToken: string, tableId: string, recordIds: readonly string[]): Promise<void>
134
+ }
135
+
136
+ interface RawMatrix {
137
+ fields?: unknown
138
+ record_id_list?: unknown
139
+ data?: unknown
140
+ has_more?: unknown
141
+ rev?: unknown
142
+ timezone?: unknown
143
+ ignored_fields?: unknown
144
+ }
145
+
146
+ function stringArray(value: unknown): string[] | undefined {
147
+ return Array.isArray(value) && value.every(item => typeof item === 'string') ? value as string[] : undefined
148
+ }
149
+
150
+ function ignoredFieldsOf(value: unknown): string[] {
151
+ if (!Array.isArray(value)) return []
152
+ return value.flatMap((item) => {
153
+ if (typeof item !== 'object' || item === null) return []
154
+ const { name, id, reason } = item as { name?: unknown; id?: unknown; reason?: unknown }
155
+ const label = typeof name === 'string' && name !== '' ? name : typeof id === 'string' ? id : '?'
156
+ return [typeof reason === 'string' && reason !== '' ? `${label}: ${reason}` : label]
157
+ })
158
+ }
159
+
160
+ function recordPage(raw: RawMatrix, where: string): BaseRecordPage {
161
+ const names = stringArray(raw.fields) ?? []
162
+ const ids = stringArray(raw.record_id_list) ?? []
163
+ const rows = Array.isArray(raw.data) ? raw.data : []
164
+ if (rows.length !== ids.length) throw new FeishuApiError(`${where}: record ids and rows differ in length`)
165
+ const records = ids.map((recordId, index) => {
166
+ const row: unknown = rows[index]
167
+ if (!Array.isArray(row) || row.length !== names.length) throw new FeishuApiError(`${where}: row ${index} does not match the field list`)
168
+ return { recordId, fields: Object.fromEntries(names.map((name, column) => [name, row[column] ?? null])) }
169
+ })
170
+ return {
171
+ records,
172
+ hasMore: raw.has_more === true,
173
+ ...(typeof raw.rev === 'number' ? { rev: raw.rev } : {}),
174
+ ...(typeof raw.timezone === 'string' && raw.timezone !== '' ? { timezone: raw.timezone } : {}),
175
+ ignoredFields: ignoredFieldsOf(raw.ignored_fields),
176
+ }
177
+ }
178
+
179
+ /**
180
+ * {@link BaseApi} over the SDK's generic authenticated request.
181
+ *
182
+ * @param request `Client.request` of the connected SDK client.
183
+ * @returns The API.
184
+ */
185
+ export function createBaseApi(request: WikiRawRequest): BaseApi {
186
+ const tablePath = (baseToken: string, tableId: string): string =>
187
+ `/open-apis/base/v3/bases/${segment(baseToken)}/tables/${segment(tableId)}`
188
+ return {
189
+ async listTables(baseToken) {
190
+ const data = await call<{ tables?: unknown; items?: unknown }>(request, {
191
+ method: 'GET',
192
+ url: `/open-apis/base/v3/bases/${segment(baseToken)}/tables`,
193
+ params: { offset: 0, limit: 300 },
194
+ })
195
+ const raw = Array.isArray(data.tables) ? data.tables : Array.isArray(data.items) ? data.items : []
196
+ return raw.flatMap((item: unknown) => {
197
+ if (typeof item !== 'object' || item === null) return []
198
+ const { id, table_id, name, table_name } = item as Record<string, unknown>
199
+ const tableId = typeof id === 'string' ? id : typeof table_id === 'string' ? table_id : undefined
200
+ if (tableId === undefined) return []
201
+ return [{ tableId, name: typeof name === 'string' ? name : typeof table_name === 'string' ? table_name : '' }]
202
+ })
203
+ },
204
+ async listFields(baseToken, tableId) {
205
+ const data = await call<{ fields?: unknown; items?: unknown }>(request, {
206
+ method: 'GET',
207
+ url: `${tablePath(baseToken, tableId)}/fields`,
208
+ params: { offset: 0, limit: 300 },
209
+ })
210
+ const raw = Array.isArray(data.fields) ? data.fields : Array.isArray(data.items) ? data.items : []
211
+ return raw.flatMap((item: unknown) => {
212
+ if (typeof item !== 'object' || item === null) return []
213
+ const { id, field_id, name, field_name, type, ...properties } = item as Record<string, unknown>
214
+ const fieldId = typeof id === 'string' ? id : typeof field_id === 'string' ? field_id : undefined
215
+ if (fieldId === undefined) return []
216
+ return [{
217
+ fieldId,
218
+ name: typeof name === 'string' ? name : typeof field_name === 'string' ? field_name : '',
219
+ type: typeof type === 'string' ? type : 'not_support',
220
+ properties,
221
+ }]
222
+ })
223
+ },
224
+ async listRecords(baseToken, tableId, query) {
225
+ const url = `${tablePath(baseToken, tableId)}/records`
226
+ const params = new URLSearchParams({ offset: String(query.offset), limit: String(query.limit) })
227
+ for (const field of query.fields ?? []) params.append('field_id', field)
228
+ if (query.viewId !== undefined) params.set('view_id', query.viewId)
229
+ if (query.filter !== undefined) params.set('filter', JSON.stringify(query.filter))
230
+ if (query.sort !== undefined) params.set('sort', JSON.stringify(query.sort))
231
+ const data = await call<RawMatrix>(request, { method: 'GET', url: `${url}?${params.toString()}` })
232
+ return recordPage(data, url)
233
+ },
234
+ async searchRecords(baseToken, tableId, search) {
235
+ const url = `${tablePath(baseToken, tableId)}/records/search`
236
+ const data = await call<RawMatrix>(request, {
237
+ method: 'POST',
238
+ url,
239
+ data: {
240
+ keyword: search.keyword,
241
+ search_fields: search.searchFields,
242
+ ...(search.fields === undefined ? {} : { select_fields: search.fields }),
243
+ ...(search.viewId === undefined ? {} : { view_id: search.viewId }),
244
+ ...(search.filter === undefined ? {} : { filter: search.filter }),
245
+ ...(search.sort === undefined ? {} : { sort: search.sort }),
246
+ offset: search.offset,
247
+ limit: search.limit,
248
+ },
249
+ })
250
+ return recordPage(data, url)
251
+ },
252
+ async createRecords(baseToken, tableId, records) {
253
+ const url = `${tablePath(baseToken, tableId)}/records/batch_create`
254
+ const data = await call<{ record_id_list?: unknown }>(request, { method: 'POST', url, data: { create_records: records } })
255
+ return stringArray(data.record_id_list) ?? []
256
+ },
257
+ async updateRecords(baseToken, tableId, updates) {
258
+ const url = `${tablePath(baseToken, tableId)}/records/batch_update`
259
+ const data = await call<{ ignored_fields?: unknown }>(request, { method: 'POST', url, data: { update_records: updates } })
260
+ return ignoredFieldsOf(data.ignored_fields)
261
+ },
262
+ async deleteRecords(baseToken, tableId, recordIds) {
263
+ const url = `${tablePath(baseToken, tableId)}/records/batch_delete`
264
+ await call<unknown>(request, { method: 'POST', url, data: { record_id_list: recordIds } })
265
+ },
266
+ }
267
+ }