quickbase-js 2.4.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -76
- package/dist/quickbase.esm.js +3 -3
- package/dist/quickbase.min.js +3 -3
- package/dist/quickbase.min.js.map +1 -1
- package/package.json +13 -9
- package/src/auth/index.ts +55 -0
- package/src/auth/sso-token.ts +143 -0
- package/src/auth/temp-token.ts +160 -0
- package/src/auth/ticket.ts +329 -0
- package/src/auth/token-cache.ts +97 -0
- package/src/auth/types.ts +50 -0
- package/src/auth/user-token.ts +38 -0
- package/src/cli/schema.ts +512 -0
- package/src/client/index.ts +36 -0
- package/src/client/pagination.ts +337 -0
- package/src/client/request.ts +544 -0
- package/src/client/retry.ts +169 -0
- package/src/client/throttle.ts +105 -0
- package/src/core/config.ts +160 -0
- package/src/core/dates.ts +63 -0
- package/src/core/errors.ts +349 -0
- package/src/core/index.ts +50 -0
- package/src/core/logger.ts +113 -0
- package/src/core/schema.ts +213 -0
- package/src/core/transform.ts +295 -0
- package/src/core/types.ts +337 -0
- package/src/generated/client.ts +1785 -0
- package/src/generated/operations.json +672 -0
- package/src/generated/types.ts +4884 -0
- package/src/index.ts +259 -0
- package/src/xml/codepages.ts +119 -0
- package/src/xml/dbvars.ts +75 -0
- package/src/xml/errors.ts +133 -0
- package/src/xml/fields.ts +121 -0
- package/src/xml/groups.ts +560 -0
- package/src/xml/html.ts +152 -0
- package/src/xml/index.ts +1196 -0
- package/src/xml/metadata.ts +111 -0
- package/src/xml/records.ts +301 -0
- package/src/xml/request.ts +238 -0
- package/src/xml/roles.ts +321 -0
- package/src/xml/schema.ts +436 -0
- package/src/xml/transform.ts +381 -0
- package/src/xml/types.ts +602 -0
- package/src/xml/users.ts +221 -0
- package/src/xml/webhooks.ts +260 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XML API App Metadata
|
|
3
|
+
*
|
|
4
|
+
* Endpoints for getting app metadata and ancestry info.
|
|
5
|
+
*
|
|
6
|
+
* XML-API: Remove this file when QuickBase discontinues the XML API.
|
|
7
|
+
* To find all XML API code, search for: XML-API
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
XmlCaller,
|
|
12
|
+
GetAppDTMInfoResult,
|
|
13
|
+
GetAncestorInfoResult,
|
|
14
|
+
} from './types.js';
|
|
15
|
+
import { checkError } from './errors.js';
|
|
16
|
+
import {
|
|
17
|
+
buildRequest,
|
|
18
|
+
escapeXml,
|
|
19
|
+
parseBaseResponse,
|
|
20
|
+
getTagContent,
|
|
21
|
+
getAllElements,
|
|
22
|
+
getAttribute,
|
|
23
|
+
} from './request.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get modification timestamps (fast, no auth required for some info).
|
|
27
|
+
*
|
|
28
|
+
* This is a lightweight call to check if data has changed.
|
|
29
|
+
*
|
|
30
|
+
* @see https://help.quickbase.com/docs/api-getappdtminfo
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const info = await getAppDTMInfo(caller, 'bqapp123');
|
|
35
|
+
* console.log(`Last modified: ${info.lastModifiedTime}`);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export async function getAppDTMInfo(
|
|
39
|
+
caller: XmlCaller,
|
|
40
|
+
appId: string
|
|
41
|
+
): Promise<GetAppDTMInfoResult> {
|
|
42
|
+
// GetAppDTMInfo is invoked on db/main with appId in the body
|
|
43
|
+
const inner = `<dbid>${escapeXml(appId)}</dbid>`;
|
|
44
|
+
const body = buildRequest(inner);
|
|
45
|
+
const xml = await caller.doXml('main', 'API_GetAppDTMInfo', body);
|
|
46
|
+
|
|
47
|
+
const base = parseBaseResponse(xml);
|
|
48
|
+
checkError(base);
|
|
49
|
+
|
|
50
|
+
// Parse tables
|
|
51
|
+
const tableElements = getAllElements(xml, 'table');
|
|
52
|
+
const tables = tableElements.map((tableXml) => ({
|
|
53
|
+
id: getAttribute(tableXml, 'id'),
|
|
54
|
+
lastModifiedTime: getTagContent(tableXml, 'lastModifiedTime') || undefined,
|
|
55
|
+
lastRecModTime: getTagContent(tableXml, 'lastRecModTime') || undefined,
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
appId: getTagContent(xml, 'app_id') || appId,
|
|
60
|
+
lastModifiedTime: getTagContent(xml, 'lastModifiedTime') || undefined,
|
|
61
|
+
lastRecModTime: getTagContent(xml, 'lastRecModTime') || undefined,
|
|
62
|
+
tables,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get app copy/template lineage info.
|
|
68
|
+
*
|
|
69
|
+
* @see https://help.quickbase.com/docs/api-getancestorinfo
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const info = await getAncestorInfo(caller, 'bqapp123');
|
|
74
|
+
* if (info.ancestorAppId) {
|
|
75
|
+
* console.log(`This app was copied from: ${info.ancestorAppId}`);
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export async function getAncestorInfo(
|
|
80
|
+
caller: XmlCaller,
|
|
81
|
+
appId: string
|
|
82
|
+
): Promise<GetAncestorInfoResult> {
|
|
83
|
+
const body = buildRequest('');
|
|
84
|
+
const xml = await caller.doXml(appId, 'API_GetAncestorInfo', body);
|
|
85
|
+
|
|
86
|
+
const base = parseBaseResponse(xml);
|
|
87
|
+
checkError(base);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
ancestorAppId: getTagContent(xml, 'ancestorappid') || undefined,
|
|
91
|
+
oldestAncestorAppId: getTagContent(xml, 'oldestancestorappid') || undefined,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Clear ticket cookie (browser-focused).
|
|
97
|
+
*
|
|
98
|
+
* @see https://help.quickbase.com/docs/api-signout
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* await signOut(caller);
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export async function signOut(caller: XmlCaller): Promise<void> {
|
|
106
|
+
const body = buildRequest('');
|
|
107
|
+
const xml = await caller.doXml('main', 'API_SignOut', body);
|
|
108
|
+
|
|
109
|
+
const base = parseBaseResponse(xml);
|
|
110
|
+
checkError(base);
|
|
111
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XML API Record Information
|
|
3
|
+
*
|
|
4
|
+
* Endpoints for querying and managing record information.
|
|
5
|
+
*
|
|
6
|
+
* XML-API: Remove this file when QuickBase discontinues the XML API.
|
|
7
|
+
* To find all XML API code, search for: XML-API
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
XmlCaller,
|
|
12
|
+
RecordField,
|
|
13
|
+
DoQueryCountResult,
|
|
14
|
+
GetRecordInfoResult,
|
|
15
|
+
ImportFromCSVResult,
|
|
16
|
+
CopyMasterDetailResult,
|
|
17
|
+
} from './types.js';
|
|
18
|
+
import { checkError } from './errors.js';
|
|
19
|
+
import {
|
|
20
|
+
buildRequest,
|
|
21
|
+
escapeXml,
|
|
22
|
+
parseBaseResponse,
|
|
23
|
+
getTagContent,
|
|
24
|
+
getAttribute,
|
|
25
|
+
getAllElements,
|
|
26
|
+
parseXmlNumber,
|
|
27
|
+
xmlElementCDATA,
|
|
28
|
+
} from './request.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse a field element from GetRecordInfo response
|
|
32
|
+
*/
|
|
33
|
+
function parseRecordField(fieldXml: string): RecordField {
|
|
34
|
+
const field: RecordField = {
|
|
35
|
+
id: parseXmlNumber(getTagContent(fieldXml, 'fid')),
|
|
36
|
+
name: getTagContent(fieldXml, 'name'),
|
|
37
|
+
type: getTagContent(fieldXml, 'type'),
|
|
38
|
+
value: getTagContent(fieldXml, 'value'),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const printable = getTagContent(fieldXml, 'printable');
|
|
42
|
+
if (printable) field.printable = printable;
|
|
43
|
+
|
|
44
|
+
return field;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get count of matching records without fetching data.
|
|
49
|
+
*
|
|
50
|
+
* @see https://help.quickbase.com/docs/api-doquerycount
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const result = await doQueryCount(caller, 'bqtable123', "{'7'.XCT.'blue car'}");
|
|
55
|
+
* console.log(`Found ${result.numMatches} matching records`);
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export async function doQueryCount(
|
|
59
|
+
caller: XmlCaller,
|
|
60
|
+
tableId: string,
|
|
61
|
+
query?: string
|
|
62
|
+
): Promise<DoQueryCountResult> {
|
|
63
|
+
let inner = '';
|
|
64
|
+
if (query) {
|
|
65
|
+
inner = `<query>${escapeXml(query)}</query>`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const body = buildRequest(inner);
|
|
69
|
+
const xml = await caller.doXml(tableId, 'API_DoQueryCount', body);
|
|
70
|
+
|
|
71
|
+
const base = parseBaseResponse(xml);
|
|
72
|
+
checkError(base);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
numMatches: parseXmlNumber(getTagContent(xml, 'numMatches')),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get a record with field metadata.
|
|
81
|
+
*
|
|
82
|
+
* Returns all fields for the specified record, including field names and types.
|
|
83
|
+
*
|
|
84
|
+
* @see https://help.quickbase.com/docs/api-getrecordinfo
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* const result = await getRecordInfo(caller, 'bqtable123', 42);
|
|
89
|
+
* console.log(`Record ${result.recordId} has ${result.numFields} fields`);
|
|
90
|
+
* for (const field of result.fields) {
|
|
91
|
+
* console.log(` ${field.name}: ${field.value}`);
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export async function getRecordInfo(
|
|
96
|
+
caller: XmlCaller,
|
|
97
|
+
tableId: string,
|
|
98
|
+
recordId: number
|
|
99
|
+
): Promise<GetRecordInfoResult> {
|
|
100
|
+
const inner = `<rid>${recordId}</rid>`;
|
|
101
|
+
const body = buildRequest(inner);
|
|
102
|
+
const xml = await caller.doXml(tableId, 'API_GetRecordInfo', body);
|
|
103
|
+
|
|
104
|
+
const base = parseBaseResponse(xml);
|
|
105
|
+
checkError(base);
|
|
106
|
+
|
|
107
|
+
const fieldElements = getAllElements(xml, 'field');
|
|
108
|
+
const fields = fieldElements.map(parseRecordField);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
recordId: parseXmlNumber(getTagContent(xml, 'rid')),
|
|
112
|
+
numFields: parseXmlNumber(getTagContent(xml, 'num_fields')),
|
|
113
|
+
updateId: getTagContent(xml, 'update_id') || undefined,
|
|
114
|
+
fields,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get a record by key field value.
|
|
120
|
+
*
|
|
121
|
+
* @see https://help.quickbase.com/docs/api-getrecordinfo
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* const result = await getRecordInfoByKey(caller, 'bqtable123', 'ABC-123');
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export async function getRecordInfoByKey(
|
|
129
|
+
caller: XmlCaller,
|
|
130
|
+
tableId: string,
|
|
131
|
+
keyValue: string
|
|
132
|
+
): Promise<GetRecordInfoResult> {
|
|
133
|
+
const inner = `<key>${escapeXml(keyValue)}</key>`;
|
|
134
|
+
const body = buildRequest(inner);
|
|
135
|
+
const xml = await caller.doXml(tableId, 'API_GetRecordInfo', body);
|
|
136
|
+
|
|
137
|
+
const base = parseBaseResponse(xml);
|
|
138
|
+
checkError(base);
|
|
139
|
+
|
|
140
|
+
const fieldElements = getAllElements(xml, 'field');
|
|
141
|
+
const fields = fieldElements.map(parseRecordField);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
recordId: parseXmlNumber(getTagContent(xml, 'rid')),
|
|
145
|
+
numFields: parseXmlNumber(getTagContent(xml, 'num_fields')),
|
|
146
|
+
updateId: getTagContent(xml, 'update_id') || undefined,
|
|
147
|
+
fields,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Bulk import/update records from CSV data.
|
|
153
|
+
*
|
|
154
|
+
* You can add and update records in the same call.
|
|
155
|
+
* Leave Record ID empty to add new records.
|
|
156
|
+
*
|
|
157
|
+
* @see https://help.quickbase.com/docs/api-importfromcsv
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* const result = await importFromCSV(caller, 'bqtable123', {
|
|
162
|
+
* recordsCsv: 'Name,Email\nJohn,john@example.com\nJane,jane@example.com',
|
|
163
|
+
* clist: [7, 8],
|
|
164
|
+
* skipFirst: true,
|
|
165
|
+
* });
|
|
166
|
+
* console.log(`Added ${result.numRecsAdded}, updated ${result.numRecsUpdated}`);
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export async function importFromCSV(
|
|
170
|
+
caller: XmlCaller,
|
|
171
|
+
tableId: string,
|
|
172
|
+
options: {
|
|
173
|
+
recordsCsv: string;
|
|
174
|
+
clist?: number[];
|
|
175
|
+
skipFirst?: boolean;
|
|
176
|
+
mergeFieldId?: number;
|
|
177
|
+
decimalPercent?: boolean;
|
|
178
|
+
}
|
|
179
|
+
): Promise<ImportFromCSVResult> {
|
|
180
|
+
let inner = xmlElementCDATA('records_csv', options.recordsCsv);
|
|
181
|
+
|
|
182
|
+
if (options.clist && options.clist.length > 0) {
|
|
183
|
+
inner += `<clist>${options.clist.join('.')}</clist>`;
|
|
184
|
+
}
|
|
185
|
+
if (options.skipFirst) {
|
|
186
|
+
inner += '<skipfirst>1</skipfirst>';
|
|
187
|
+
}
|
|
188
|
+
if (options.mergeFieldId !== undefined) {
|
|
189
|
+
inner += `<mergeFieldId>${options.mergeFieldId}</mergeFieldId>`;
|
|
190
|
+
}
|
|
191
|
+
if (options.decimalPercent) {
|
|
192
|
+
inner += '<decimalPercent>1</decimalPercent>';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const body = buildRequest(inner);
|
|
196
|
+
const xml = await caller.doXml(tableId, 'API_ImportFromCSV', body);
|
|
197
|
+
|
|
198
|
+
const base = parseBaseResponse(xml);
|
|
199
|
+
checkError(base);
|
|
200
|
+
|
|
201
|
+
// Parse record IDs
|
|
202
|
+
const ridElements = getAllElements(xml, 'rid');
|
|
203
|
+
const recordIds = ridElements.map((ridXml) => {
|
|
204
|
+
const rid = parseXmlNumber(ridXml.replace(/<rid[^>]*>/, '').replace(/<\/rid>/, ''));
|
|
205
|
+
const updateId = getAttribute(ridXml, 'update_id') || undefined;
|
|
206
|
+
return { recordId: rid, updateId };
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
numRecsInput: parseXmlNumber(getTagContent(xml, 'num_recs_input')),
|
|
211
|
+
numRecsAdded: parseXmlNumber(getTagContent(xml, 'num_recs_added')),
|
|
212
|
+
numRecsUpdated: parseXmlNumber(getTagContent(xml, 'num_recs_updated')),
|
|
213
|
+
recordIds,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Execute a saved import definition.
|
|
219
|
+
*
|
|
220
|
+
* @see https://help.quickbase.com/docs/api-runimport
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```typescript
|
|
224
|
+
* const result = await runImport(caller, 'bqtable123', 5);
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
export async function runImport(
|
|
228
|
+
caller: XmlCaller,
|
|
229
|
+
tableId: string,
|
|
230
|
+
importId: number
|
|
231
|
+
): Promise<ImportFromCSVResult> {
|
|
232
|
+
const inner = `<id>${importId}</id>`;
|
|
233
|
+
const body = buildRequest(inner);
|
|
234
|
+
const xml = await caller.doXml(tableId, 'API_RunImport', body);
|
|
235
|
+
|
|
236
|
+
const base = parseBaseResponse(xml);
|
|
237
|
+
checkError(base);
|
|
238
|
+
|
|
239
|
+
// Parse record IDs
|
|
240
|
+
const ridElements = getAllElements(xml, 'rid');
|
|
241
|
+
const recordIds = ridElements.map((ridXml) => {
|
|
242
|
+
const rid = parseXmlNumber(ridXml.replace(/<rid[^>]*>/, '').replace(/<\/rid>/, ''));
|
|
243
|
+
const updateId = getAttribute(ridXml, 'update_id') || undefined;
|
|
244
|
+
return { recordId: rid, updateId };
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
numRecsInput: parseXmlNumber(getTagContent(xml, 'num_recs_input')),
|
|
249
|
+
numRecsAdded: parseXmlNumber(getTagContent(xml, 'num_recs_added')),
|
|
250
|
+
numRecsUpdated: parseXmlNumber(getTagContent(xml, 'num_recs_updated')),
|
|
251
|
+
recordIds,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Copy a master record with its detail records.
|
|
257
|
+
*
|
|
258
|
+
* @see https://help.quickbase.com/docs/api-copymasterdetail
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```typescript
|
|
262
|
+
* const result = await copyMasterDetail(caller, 'bqtable123', {
|
|
263
|
+
* rid: 42,
|
|
264
|
+
* recurse: true,
|
|
265
|
+
* });
|
|
266
|
+
* console.log(`Copied to record ${result.newRid}`);
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
export async function copyMasterDetail(
|
|
270
|
+
caller: XmlCaller,
|
|
271
|
+
tableId: string,
|
|
272
|
+
options: {
|
|
273
|
+
rid: number;
|
|
274
|
+
copyfid?: number;
|
|
275
|
+
recurse?: boolean;
|
|
276
|
+
relfids?: number[];
|
|
277
|
+
}
|
|
278
|
+
): Promise<CopyMasterDetailResult> {
|
|
279
|
+
let inner = `<rid>${options.rid}</rid>`;
|
|
280
|
+
|
|
281
|
+
if (options.copyfid !== undefined) {
|
|
282
|
+
inner += `<copyfid>${options.copyfid}</copyfid>`;
|
|
283
|
+
}
|
|
284
|
+
if (options.recurse) {
|
|
285
|
+
inner += '<recurse>1</recurse>';
|
|
286
|
+
}
|
|
287
|
+
if (options.relfids && options.relfids.length > 0) {
|
|
288
|
+
inner += `<relfids>${options.relfids.join('.')}</relfids>`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const body = buildRequest(inner);
|
|
292
|
+
const xml = await caller.doXml(tableId, 'API_CopyMasterDetail', body);
|
|
293
|
+
|
|
294
|
+
const base = parseBaseResponse(xml);
|
|
295
|
+
checkError(base);
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
newRid: parseXmlNumber(getTagContent(xml, 'newrid')),
|
|
299
|
+
numCreated: parseXmlNumber(getTagContent(xml, 'num_created')),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XML API Request/Response Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for building XML requests and parsing XML responses
|
|
5
|
+
* for the legacy QuickBase XML API.
|
|
6
|
+
*
|
|
7
|
+
* XML-API: Remove this file when QuickBase discontinues the XML API.
|
|
8
|
+
* To find all XML API code, search for: XML-API
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { BaseResponse } from './types.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build an XML API request body.
|
|
15
|
+
* Wraps the inner XML content in <qdbapi> tags.
|
|
16
|
+
*
|
|
17
|
+
* @param inner - The inner XML content (without qdbapi wrapper)
|
|
18
|
+
* @returns Complete XML request body
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* buildRequest('<userid>123</userid><roleid>10</roleid>')
|
|
22
|
+
* // Returns: '<qdbapi><userid>123</userid><roleid>10</roleid></qdbapi>'
|
|
23
|
+
*/
|
|
24
|
+
export function buildRequest(inner: string): string {
|
|
25
|
+
return `<qdbapi>${inner}</qdbapi>`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Inject an app token into an XML request body.
|
|
30
|
+
* The body is expected to have the format <qdbapi>...</qdbapi>.
|
|
31
|
+
* The apptoken is inserted right after the opening <qdbapi> tag.
|
|
32
|
+
*
|
|
33
|
+
* @param body - The XML request body
|
|
34
|
+
* @param appToken - The app token to inject
|
|
35
|
+
* @returns Body with apptoken element inserted
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* injectAppToken('<qdbapi><query>{}</query></qdbapi>', 'abc123')
|
|
39
|
+
* // Returns: '<qdbapi><apptoken>abc123</apptoken><query>{}</query></qdbapi>'
|
|
40
|
+
*/
|
|
41
|
+
export function injectAppToken(body: string, appToken: string): string {
|
|
42
|
+
const openTag = '<qdbapi>';
|
|
43
|
+
const idx = body.indexOf(openTag);
|
|
44
|
+
if (idx === -1) {
|
|
45
|
+
return body; // Can't find tag, return unchanged
|
|
46
|
+
}
|
|
47
|
+
const insertPos = idx + openTag.length;
|
|
48
|
+
const appTokenElem = `<apptoken>${escapeXml(appToken)}</apptoken>`;
|
|
49
|
+
return body.slice(0, insertPos) + appTokenElem + body.slice(insertPos);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Inject a user token into an XML request body.
|
|
54
|
+
* The body is expected to have the format <qdbapi>...</qdbapi>.
|
|
55
|
+
* The usertoken is inserted right after the opening <qdbapi> tag.
|
|
56
|
+
*
|
|
57
|
+
* The XML API requires authentication tokens as body elements, not headers.
|
|
58
|
+
* See: https://help.quickbase.com/docs/api-getdbpage
|
|
59
|
+
*
|
|
60
|
+
* @param body - The XML request body
|
|
61
|
+
* @param userToken - The user token to inject
|
|
62
|
+
* @returns Body with usertoken element inserted
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* injectUserToken('<qdbapi><query>{}</query></qdbapi>', 'b5d4r...')
|
|
66
|
+
* // Returns: '<qdbapi><usertoken>b5d4r...</usertoken><query>{}</query></qdbapi>'
|
|
67
|
+
*/
|
|
68
|
+
export function injectUserToken(body: string, userToken: string): string {
|
|
69
|
+
const openTag = '<qdbapi>';
|
|
70
|
+
const idx = body.indexOf(openTag);
|
|
71
|
+
if (idx === -1) {
|
|
72
|
+
return body; // Can't find tag, return unchanged
|
|
73
|
+
}
|
|
74
|
+
const insertPos = idx + openTag.length;
|
|
75
|
+
const userTokenElem = `<usertoken>${escapeXml(userToken)}</usertoken>`;
|
|
76
|
+
return body.slice(0, insertPos) + userTokenElem + body.slice(insertPos);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Escape special characters for safe inclusion in XML.
|
|
81
|
+
*
|
|
82
|
+
* @param str - The string to escape
|
|
83
|
+
* @returns XML-escaped string
|
|
84
|
+
*/
|
|
85
|
+
export function escapeXml(str: string): string {
|
|
86
|
+
return str
|
|
87
|
+
.replace(/&/g, '&')
|
|
88
|
+
.replace(/</g, '<')
|
|
89
|
+
.replace(/>/g, '>')
|
|
90
|
+
.replace(/"/g, '"')
|
|
91
|
+
.replace(/'/g, ''');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract text content from a single XML tag.
|
|
96
|
+
* Uses regex for simple extraction without DOM parsing.
|
|
97
|
+
*
|
|
98
|
+
* @param xml - The XML string to search
|
|
99
|
+
* @param tag - The tag name to extract
|
|
100
|
+
* @returns The text content or empty string if not found
|
|
101
|
+
*/
|
|
102
|
+
export function getTagContent(xml: string, tag: string): string {
|
|
103
|
+
// Match tag with optional attributes, capture inner content
|
|
104
|
+
const match = xml.match(new RegExp(`<${tag}(?:\\s[^>]*)?>([^<]*)</${tag}>`));
|
|
105
|
+
return match?.[1] ?? '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extract text content with CDATA support.
|
|
110
|
+
* Some XML responses wrap content in CDATA blocks.
|
|
111
|
+
*
|
|
112
|
+
* @param xml - The XML string to search
|
|
113
|
+
* @param tag - The tag name to extract
|
|
114
|
+
* @returns The text content or empty string if not found
|
|
115
|
+
*/
|
|
116
|
+
export function getTagContentWithCDATA(xml: string, tag: string): string {
|
|
117
|
+
// Try CDATA first
|
|
118
|
+
const cdataMatch = xml.match(
|
|
119
|
+
new RegExp(`<${tag}>\\s*<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>\\s*</${tag}>`)
|
|
120
|
+
);
|
|
121
|
+
if (cdataMatch && cdataMatch[1] !== undefined) {
|
|
122
|
+
return cdataMatch[1];
|
|
123
|
+
}
|
|
124
|
+
// Fall back to regular content
|
|
125
|
+
return getTagContent(xml, tag);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Extract an attribute value from an XML element.
|
|
130
|
+
*
|
|
131
|
+
* @param xml - The XML element string
|
|
132
|
+
* @param attr - The attribute name
|
|
133
|
+
* @returns The attribute value or empty string if not found
|
|
134
|
+
*/
|
|
135
|
+
export function getAttribute(xml: string, attr: string): string {
|
|
136
|
+
const match = xml.match(new RegExp(`${attr}="([^"]*)"`));
|
|
137
|
+
return match?.[1] ?? '';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Extract all occurrences of a tag as an array of strings.
|
|
142
|
+
*
|
|
143
|
+
* @param xml - The XML string to search
|
|
144
|
+
* @param tag - The tag name to extract
|
|
145
|
+
* @returns Array of XML element strings (including tags)
|
|
146
|
+
*/
|
|
147
|
+
export function getAllElements(xml: string, tag: string): string[] {
|
|
148
|
+
const regex = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, 'g');
|
|
149
|
+
const elements: string[] = [];
|
|
150
|
+
let match;
|
|
151
|
+
while ((match = regex.exec(xml)) !== null) {
|
|
152
|
+
elements.push(match[0]);
|
|
153
|
+
}
|
|
154
|
+
return elements;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse the base response fields from an XML API response.
|
|
159
|
+
*
|
|
160
|
+
* @param xml - The XML response string
|
|
161
|
+
* @returns BaseResponse with action, errcode, errtext, errdetail
|
|
162
|
+
*/
|
|
163
|
+
export function parseBaseResponse(xml: string): BaseResponse {
|
|
164
|
+
const errdetail = getTagContent(xml, 'errdetail');
|
|
165
|
+
return {
|
|
166
|
+
action: getTagContent(xml, 'action'),
|
|
167
|
+
errcode: parseInt(getTagContent(xml, 'errcode'), 10) || 0,
|
|
168
|
+
errtext: getTagContent(xml, 'errtext'),
|
|
169
|
+
errdetail: errdetail || undefined,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Create an XML element with content.
|
|
175
|
+
* Escapes the content automatically.
|
|
176
|
+
*
|
|
177
|
+
* @param tag - The tag name
|
|
178
|
+
* @param content - The content (will be escaped)
|
|
179
|
+
* @returns XML element string
|
|
180
|
+
*/
|
|
181
|
+
export function xmlElement(tag: string, content: string | number): string {
|
|
182
|
+
return `<${tag}>${escapeXml(String(content))}</${tag}>`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Create an XML element with CDATA content.
|
|
187
|
+
* Use for large text content that may contain special characters.
|
|
188
|
+
*
|
|
189
|
+
* @param tag - The tag name
|
|
190
|
+
* @param content - The content (will be wrapped in CDATA)
|
|
191
|
+
* @returns XML element string with CDATA
|
|
192
|
+
*/
|
|
193
|
+
export function xmlElementCDATA(tag: string, content: string): string {
|
|
194
|
+
// CDATA cannot contain the sequence ]]> - if present, split it
|
|
195
|
+
const escaped = content.replace(/\]\]>/g, ']]]]><![CDATA[>');
|
|
196
|
+
return `<${tag}><![CDATA[${escaped}]]></${tag}>`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Create an XML element with an attribute.
|
|
201
|
+
*
|
|
202
|
+
* @param tag - The tag name
|
|
203
|
+
* @param attrName - The attribute name
|
|
204
|
+
* @param attrValue - The attribute value
|
|
205
|
+
* @param content - The content (will be escaped)
|
|
206
|
+
* @returns XML element string with attribute
|
|
207
|
+
*/
|
|
208
|
+
export function xmlElementWithAttr(
|
|
209
|
+
tag: string,
|
|
210
|
+
attrName: string,
|
|
211
|
+
attrValue: string | number,
|
|
212
|
+
content: string | number
|
|
213
|
+
): string {
|
|
214
|
+
return `<${tag} ${attrName}="${escapeXml(String(attrValue))}">${escapeXml(String(content))}</${tag}>`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Parse a boolean-like value from XML.
|
|
219
|
+
* QuickBase uses "1" for true, "0" or empty for false.
|
|
220
|
+
*
|
|
221
|
+
* @param value - The string value to parse
|
|
222
|
+
* @returns boolean
|
|
223
|
+
*/
|
|
224
|
+
export function parseXmlBool(value: string): boolean {
|
|
225
|
+
return value === '1' || value.toLowerCase() === 'true';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Parse a numeric value from XML.
|
|
230
|
+
*
|
|
231
|
+
* @param value - The string value to parse
|
|
232
|
+
* @param defaultValue - Default value if parsing fails
|
|
233
|
+
* @returns number
|
|
234
|
+
*/
|
|
235
|
+
export function parseXmlNumber(value: string, defaultValue = 0): number {
|
|
236
|
+
const num = parseInt(value, 10);
|
|
237
|
+
return isNaN(num) ? defaultValue : num;
|
|
238
|
+
}
|