quickbase-js 2.3.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.
Files changed (62) hide show
  1. package/README.md +145 -1107
  2. package/dist/auth/ticket.d.ts.map +1 -1
  3. package/dist/auth/ticket.js +4 -3
  4. package/dist/auth/ticket.js.map +1 -1
  5. package/dist/client/request.d.ts +4 -0
  6. package/dist/client/request.d.ts.map +1 -1
  7. package/dist/client/request.js +15 -3
  8. package/dist/client/request.js.map +1 -1
  9. package/dist/generated/client.d.ts +2 -0
  10. package/dist/generated/client.d.ts.map +1 -1
  11. package/dist/generated/client.js +134 -113
  12. package/dist/generated/client.js.map +1 -1
  13. package/dist/generated/types.d.ts +2700 -2256
  14. package/dist/generated/types.d.ts.map +1 -1
  15. package/dist/generated/types.js +0 -2
  16. package/dist/generated/types.js.map +1 -1
  17. package/dist/quickbase.esm.js +3 -3
  18. package/dist/quickbase.esm.js.map +3 -3
  19. package/dist/quickbase.min.js +3 -3
  20. package/dist/quickbase.min.js.map +3 -3
  21. package/package.json +16 -13
  22. package/src/auth/index.ts +55 -0
  23. package/src/auth/sso-token.ts +143 -0
  24. package/src/auth/temp-token.ts +160 -0
  25. package/src/auth/ticket.ts +329 -0
  26. package/src/auth/token-cache.ts +97 -0
  27. package/src/auth/types.ts +50 -0
  28. package/src/auth/user-token.ts +38 -0
  29. package/src/cli/schema.ts +512 -0
  30. package/src/client/index.ts +36 -0
  31. package/src/client/pagination.ts +337 -0
  32. package/src/client/request.ts +544 -0
  33. package/src/client/retry.ts +169 -0
  34. package/src/client/throttle.ts +105 -0
  35. package/src/core/config.ts +160 -0
  36. package/src/core/dates.ts +63 -0
  37. package/src/core/errors.ts +349 -0
  38. package/src/core/index.ts +50 -0
  39. package/src/core/logger.ts +113 -0
  40. package/src/core/schema.ts +213 -0
  41. package/src/core/transform.ts +295 -0
  42. package/src/core/types.ts +337 -0
  43. package/src/generated/client.ts +1785 -0
  44. package/src/generated/operations.json +672 -0
  45. package/src/generated/types.ts +4884 -0
  46. package/src/index.ts +259 -0
  47. package/src/xml/codepages.ts +119 -0
  48. package/src/xml/dbvars.ts +75 -0
  49. package/src/xml/errors.ts +133 -0
  50. package/src/xml/fields.ts +121 -0
  51. package/src/xml/groups.ts +560 -0
  52. package/src/xml/html.ts +152 -0
  53. package/src/xml/index.ts +1196 -0
  54. package/src/xml/metadata.ts +111 -0
  55. package/src/xml/records.ts +301 -0
  56. package/src/xml/request.ts +238 -0
  57. package/src/xml/roles.ts +321 -0
  58. package/src/xml/schema.ts +436 -0
  59. package/src/xml/transform.ts +381 -0
  60. package/src/xml/types.ts +602 -0
  61. package/src/xml/users.ts +221 -0
  62. 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, '&amp;')
88
+ .replace(/</g, '&lt;')
89
+ .replace(/>/g, '&gt;')
90
+ .replace(/"/g, '&quot;')
91
+ .replace(/'/g, '&apos;');
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
+ }