@tinybirdco/sdk 0.0.38 → 0.0.39
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 +44 -0
- package/dist/api/api.d.ts +18 -1
- package/dist/api/api.d.ts.map +1 -1
- package/dist/api/api.js +56 -0
- package/dist/api/api.js.map +1 -1
- package/dist/api/api.test.js +111 -0
- package/dist/api/api.test.js.map +1 -1
- package/dist/client/base.d.ts +16 -0
- package/dist/client/base.d.ts.map +1 -1
- package/dist/client/base.js +38 -0
- package/dist/client/base.js.map +1 -1
- package/dist/client/base.test.js +4 -0
- package/dist/client/base.test.js.map +1 -1
- package/dist/client/types.d.ts +50 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/schema/project.d.ts +8 -4
- package/dist/schema/project.d.ts.map +1 -1
- package/dist/schema/project.js +8 -0
- package/dist/schema/project.js.map +1 -1
- package/dist/schema/project.test.js +14 -2
- package/dist/schema/project.test.js.map +1 -1
- package/package.json +1 -1
- package/src/api/api.test.ts +165 -0
- package/src/api/api.ts +104 -0
- package/src/client/base.test.ts +4 -0
- package/src/client/base.ts +53 -0
- package/src/client/types.ts +54 -0
- package/src/index.ts +6 -0
- package/src/schema/project.test.ts +14 -2
- package/src/schema/project.ts +19 -3
package/src/api/api.ts
CHANGED
|
@@ -2,10 +2,14 @@ import { createTinybirdFetcher, type TinybirdFetch } from "./fetcher.js";
|
|
|
2
2
|
import type {
|
|
3
3
|
AppendOptions,
|
|
4
4
|
AppendResult,
|
|
5
|
+
DeleteOptions,
|
|
6
|
+
DeleteResult,
|
|
5
7
|
IngestOptions,
|
|
6
8
|
IngestResult,
|
|
7
9
|
QueryOptions,
|
|
8
10
|
QueryResult,
|
|
11
|
+
TruncateOptions,
|
|
12
|
+
TruncateResult,
|
|
9
13
|
TinybirdErrorResponse,
|
|
10
14
|
} from "../client/types.js";
|
|
11
15
|
|
|
@@ -48,6 +52,16 @@ export interface TinybirdApiAppendOptions extends Omit<AppendOptions, 'url' | 'f
|
|
|
48
52
|
token?: string;
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
export interface TinybirdApiDeleteOptions extends Omit<DeleteOptions, 'deleteCondition'> {
|
|
56
|
+
/** Optional token override for this request */
|
|
57
|
+
token?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TinybirdApiTruncateOptions extends TruncateOptions {
|
|
61
|
+
/** Optional token override for this request */
|
|
62
|
+
token?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
51
65
|
/**
|
|
52
66
|
* Scope definition for token creation APIs
|
|
53
67
|
*/
|
|
@@ -369,6 +383,82 @@ export class TinybirdApi {
|
|
|
369
383
|
return (await response.json()) as AppendResult;
|
|
370
384
|
}
|
|
371
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Delete rows from a datasource using a SQL condition
|
|
388
|
+
*/
|
|
389
|
+
async deleteDatasource(
|
|
390
|
+
datasourceName: string,
|
|
391
|
+
options: DeleteOptions,
|
|
392
|
+
apiOptions: TinybirdApiDeleteOptions = {}
|
|
393
|
+
): Promise<DeleteResult> {
|
|
394
|
+
const deleteCondition = options.deleteCondition?.trim();
|
|
395
|
+
|
|
396
|
+
if (!deleteCondition) {
|
|
397
|
+
throw new Error("'deleteCondition' must be provided in options");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const url = new URL(
|
|
401
|
+
`/v0/datasources/${encodeURIComponent(datasourceName)}/delete`,
|
|
402
|
+
`${this.baseUrl}/`
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
const requestBody = new URLSearchParams();
|
|
406
|
+
requestBody.set("delete_condition", deleteCondition);
|
|
407
|
+
|
|
408
|
+
const dryRun = options.dryRun ?? apiOptions.dryRun;
|
|
409
|
+
if (dryRun !== undefined) {
|
|
410
|
+
requestBody.set("dry_run", String(dryRun));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const response = await this.request(url.toString(), {
|
|
414
|
+
method: "POST",
|
|
415
|
+
token: apiOptions.token,
|
|
416
|
+
headers: {
|
|
417
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
418
|
+
},
|
|
419
|
+
body: requestBody.toString(),
|
|
420
|
+
signal: this.createAbortSignal(
|
|
421
|
+
options.timeout ?? apiOptions.timeout,
|
|
422
|
+
options.signal ?? apiOptions.signal
|
|
423
|
+
),
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
if (!response.ok) {
|
|
427
|
+
await this.handleErrorResponse(response);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return (await response.json()) as DeleteResult;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Truncate all rows from a datasource
|
|
435
|
+
*/
|
|
436
|
+
async truncateDatasource(
|
|
437
|
+
datasourceName: string,
|
|
438
|
+
options: TruncateOptions = {},
|
|
439
|
+
apiOptions: TinybirdApiTruncateOptions = {}
|
|
440
|
+
): Promise<TruncateResult> {
|
|
441
|
+
const url = new URL(
|
|
442
|
+
`/v0/datasources/${encodeURIComponent(datasourceName)}/truncate`,
|
|
443
|
+
`${this.baseUrl}/`
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
const response = await this.request(url.toString(), {
|
|
447
|
+
method: "POST",
|
|
448
|
+
token: apiOptions.token,
|
|
449
|
+
signal: this.createAbortSignal(
|
|
450
|
+
options.timeout ?? apiOptions.timeout,
|
|
451
|
+
options.signal ?? apiOptions.signal
|
|
452
|
+
),
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
if (!response.ok) {
|
|
456
|
+
await this.handleErrorResponse(response);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return this.parseOptionalJson(response);
|
|
460
|
+
}
|
|
461
|
+
|
|
372
462
|
/**
|
|
373
463
|
* Create a token using Tinybird Token API.
|
|
374
464
|
* Supports both static and JWT token payloads.
|
|
@@ -438,6 +528,20 @@ export class TinybirdApi {
|
|
|
438
528
|
return formData;
|
|
439
529
|
}
|
|
440
530
|
|
|
531
|
+
private async parseOptionalJson<T extends object>(response: Response): Promise<T> {
|
|
532
|
+
const rawBody = await response.text();
|
|
533
|
+
|
|
534
|
+
if (!rawBody.trim()) {
|
|
535
|
+
return {} as T;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
return JSON.parse(rawBody) as T;
|
|
540
|
+
} catch {
|
|
541
|
+
return {} as T;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
441
545
|
private createAbortSignal(
|
|
442
546
|
timeout?: number,
|
|
443
547
|
existingSignal?: AbortSignal
|
package/src/client/base.test.ts
CHANGED
|
@@ -156,6 +156,8 @@ describe("TinybirdClient", () => {
|
|
|
156
156
|
});
|
|
157
157
|
|
|
158
158
|
expect(typeof client.datasources.append).toBe("function");
|
|
159
|
+
expect(typeof client.datasources.delete).toBe("function");
|
|
160
|
+
expect(typeof client.datasources.truncate).toBe("function");
|
|
159
161
|
});
|
|
160
162
|
|
|
161
163
|
it("datasources conforms to DatasourcesNamespace interface", () => {
|
|
@@ -167,6 +169,8 @@ describe("TinybirdClient", () => {
|
|
|
167
169
|
const datasources: DatasourcesNamespace = client.datasources;
|
|
168
170
|
expect(datasources).toBeDefined();
|
|
169
171
|
expect(typeof datasources.append).toBe("function");
|
|
172
|
+
expect(typeof datasources.delete).toBe("function");
|
|
173
|
+
expect(typeof datasources.truncate).toBe("function");
|
|
170
174
|
});
|
|
171
175
|
});
|
|
172
176
|
});
|
package/src/client/base.ts
CHANGED
|
@@ -8,10 +8,14 @@ import type {
|
|
|
8
8
|
ClientConfig,
|
|
9
9
|
ClientContext,
|
|
10
10
|
DatasourcesNamespace,
|
|
11
|
+
DeleteOptions,
|
|
12
|
+
DeleteResult,
|
|
11
13
|
QueryResult,
|
|
12
14
|
IngestResult,
|
|
13
15
|
QueryOptions,
|
|
14
16
|
IngestOptions,
|
|
17
|
+
TruncateOptions,
|
|
18
|
+
TruncateResult,
|
|
15
19
|
} from "./types.js";
|
|
16
20
|
import { TinybirdError } from "./types.js";
|
|
17
21
|
import { TinybirdApi, TinybirdApiError } from "../api/api.js";
|
|
@@ -89,6 +93,15 @@ export class TinybirdClient {
|
|
|
89
93
|
append: (datasourceName: string, options: AppendOptions): Promise<AppendResult> => {
|
|
90
94
|
return this.appendDatasource(datasourceName, options);
|
|
91
95
|
},
|
|
96
|
+
delete: (datasourceName: string, options: DeleteOptions): Promise<DeleteResult> => {
|
|
97
|
+
return this.deleteDatasource(datasourceName, options);
|
|
98
|
+
},
|
|
99
|
+
truncate: (
|
|
100
|
+
datasourceName: string,
|
|
101
|
+
options: TruncateOptions = {}
|
|
102
|
+
): Promise<TruncateResult> => {
|
|
103
|
+
return this.truncateDatasource(datasourceName, options);
|
|
104
|
+
},
|
|
92
105
|
};
|
|
93
106
|
|
|
94
107
|
// Initialize tokens namespace
|
|
@@ -133,6 +146,46 @@ export class TinybirdClient {
|
|
|
133
146
|
}
|
|
134
147
|
}
|
|
135
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Delete rows from a datasource using a SQL condition
|
|
151
|
+
*
|
|
152
|
+
* @param datasourceName - Name of the datasource
|
|
153
|
+
* @param options - Delete options including deleteCondition
|
|
154
|
+
* @returns Delete job result
|
|
155
|
+
*/
|
|
156
|
+
private async deleteDatasource(
|
|
157
|
+
datasourceName: string,
|
|
158
|
+
options: DeleteOptions
|
|
159
|
+
): Promise<DeleteResult> {
|
|
160
|
+
const token = await this.getToken();
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
return await this.getApi(token).deleteDatasource(datasourceName, options);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
this.rethrowApiError(error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Truncate all rows from a datasource
|
|
171
|
+
*
|
|
172
|
+
* @param datasourceName - Name of the datasource
|
|
173
|
+
* @param options - Truncate options
|
|
174
|
+
* @returns Truncate result
|
|
175
|
+
*/
|
|
176
|
+
private async truncateDatasource(
|
|
177
|
+
datasourceName: string,
|
|
178
|
+
options: TruncateOptions = {}
|
|
179
|
+
): Promise<TruncateResult> {
|
|
180
|
+
const token = await this.getToken();
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
return await this.getApi(token).truncateDatasource(datasourceName, options);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
this.rethrowApiError(error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
136
189
|
/**
|
|
137
190
|
* Get the effective token, resolving branch token in dev mode if needed
|
|
138
191
|
*/
|
package/src/client/types.ts
CHANGED
|
@@ -236,10 +236,64 @@ export interface AppendResult {
|
|
|
236
236
|
import_id?: string;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Options for deleting rows from a datasource
|
|
241
|
+
*/
|
|
242
|
+
export interface DeleteOptions {
|
|
243
|
+
/** SQL WHERE clause condition used to select rows to delete */
|
|
244
|
+
deleteCondition: string;
|
|
245
|
+
/** Validate and return matched rows without executing deletion */
|
|
246
|
+
dryRun?: boolean;
|
|
247
|
+
/** Request timeout in milliseconds */
|
|
248
|
+
timeout?: number;
|
|
249
|
+
/** AbortController signal for cancellation */
|
|
250
|
+
signal?: AbortSignal;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Result of deleting rows from a datasource
|
|
255
|
+
*/
|
|
256
|
+
export interface DeleteResult {
|
|
257
|
+
/** Delete job ID */
|
|
258
|
+
id?: string;
|
|
259
|
+
/** Same value as id */
|
|
260
|
+
job_id?: string;
|
|
261
|
+
/** Job status URL */
|
|
262
|
+
job_url?: string;
|
|
263
|
+
/** Job status */
|
|
264
|
+
status?: string;
|
|
265
|
+
/** Same value as id */
|
|
266
|
+
delete_id?: string;
|
|
267
|
+
/** Number of rows matched in dry run mode */
|
|
268
|
+
rows_to_be_deleted?: number;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Options for truncating a datasource
|
|
273
|
+
*/
|
|
274
|
+
export interface TruncateOptions {
|
|
275
|
+
/** Request timeout in milliseconds */
|
|
276
|
+
timeout?: number;
|
|
277
|
+
/** AbortController signal for cancellation */
|
|
278
|
+
signal?: AbortSignal;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Result of truncating a datasource
|
|
283
|
+
*/
|
|
284
|
+
export interface TruncateResult {
|
|
285
|
+
/** Optional status returned by the API */
|
|
286
|
+
status?: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
239
289
|
/**
|
|
240
290
|
* Datasources namespace interface for raw client
|
|
241
291
|
*/
|
|
242
292
|
export interface DatasourcesNamespace {
|
|
243
293
|
/** Append data to a datasource from a URL or file */
|
|
244
294
|
append(datasourceName: string, options: AppendOptions): Promise<AppendResult>;
|
|
295
|
+
/** Delete rows from a datasource using a SQL condition */
|
|
296
|
+
delete(datasourceName: string, options: DeleteOptions): Promise<DeleteResult>;
|
|
297
|
+
/** Truncate all rows from a datasource */
|
|
298
|
+
truncate(datasourceName: string, options?: TruncateOptions): Promise<TruncateResult>;
|
|
245
299
|
}
|
package/src/index.ts
CHANGED
|
@@ -208,10 +208,14 @@ export type {
|
|
|
208
208
|
ClientContext,
|
|
209
209
|
CsvDialectOptions,
|
|
210
210
|
DatasourcesNamespace,
|
|
211
|
+
DeleteOptions,
|
|
212
|
+
DeleteResult,
|
|
211
213
|
QueryResult,
|
|
212
214
|
IngestResult,
|
|
213
215
|
QueryOptions,
|
|
214
216
|
IngestOptions,
|
|
217
|
+
TruncateOptions,
|
|
218
|
+
TruncateResult,
|
|
215
219
|
ColumnMeta,
|
|
216
220
|
QueryStatistics,
|
|
217
221
|
TinybirdErrorResponse,
|
|
@@ -231,6 +235,8 @@ export type {
|
|
|
231
235
|
TinybirdApiQueryOptions,
|
|
232
236
|
TinybirdApiIngestOptions,
|
|
233
237
|
TinybirdApiAppendOptions,
|
|
238
|
+
TinybirdApiDeleteOptions,
|
|
239
|
+
TinybirdApiTruncateOptions,
|
|
234
240
|
TinybirdApiRequestInit,
|
|
235
241
|
TinybirdApiTokenScope,
|
|
236
242
|
TinybirdApiCreateTokenRequest,
|
|
@@ -94,7 +94,7 @@ describe("Project Schema", () => {
|
|
|
94
94
|
expect((project.tinybird as unknown as Record<string, unknown>).pipes).toBeUndefined();
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
it("creates datasource accessors with append
|
|
97
|
+
it("creates datasource accessors with append/delete/truncate methods", () => {
|
|
98
98
|
const events = defineDatasource("events", {
|
|
99
99
|
schema: { timestamp: t.dateTime() },
|
|
100
100
|
});
|
|
@@ -105,6 +105,8 @@ describe("Project Schema", () => {
|
|
|
105
105
|
|
|
106
106
|
expect(project.tinybird.events).toBeDefined();
|
|
107
107
|
expect(typeof project.tinybird.events.append).toBe("function");
|
|
108
|
+
expect(typeof project.tinybird.events.delete).toBe("function");
|
|
109
|
+
expect(typeof project.tinybird.events.truncate).toBe("function");
|
|
108
110
|
});
|
|
109
111
|
|
|
110
112
|
it("creates multiple datasource accessors", () => {
|
|
@@ -123,6 +125,10 @@ describe("Project Schema", () => {
|
|
|
123
125
|
expect(project.tinybird.pageViews).toBeDefined();
|
|
124
126
|
expect(typeof project.tinybird.events.append).toBe("function");
|
|
125
127
|
expect(typeof project.tinybird.pageViews.append).toBe("function");
|
|
128
|
+
expect(typeof project.tinybird.events.delete).toBe("function");
|
|
129
|
+
expect(typeof project.tinybird.pageViews.delete).toBe("function");
|
|
130
|
+
expect(typeof project.tinybird.events.truncate).toBe("function");
|
|
131
|
+
expect(typeof project.tinybird.pageViews.truncate).toBe("function");
|
|
126
132
|
});
|
|
127
133
|
|
|
128
134
|
it("throws error when accessing client before initialization", () => {
|
|
@@ -304,7 +310,7 @@ describe("Project Schema", () => {
|
|
|
304
310
|
expect((client as unknown as Record<string, unknown>).pipes).toBeUndefined();
|
|
305
311
|
});
|
|
306
312
|
|
|
307
|
-
it("creates datasource accessors with append
|
|
313
|
+
it("creates datasource accessors with append/delete/truncate methods", () => {
|
|
308
314
|
const events = defineDatasource("events", {
|
|
309
315
|
schema: { id: t.string() },
|
|
310
316
|
});
|
|
@@ -316,6 +322,8 @@ describe("Project Schema", () => {
|
|
|
316
322
|
|
|
317
323
|
expect(client.events).toBeDefined();
|
|
318
324
|
expect(typeof client.events.append).toBe("function");
|
|
325
|
+
expect(typeof client.events.delete).toBe("function");
|
|
326
|
+
expect(typeof client.events.truncate).toBe("function");
|
|
319
327
|
});
|
|
320
328
|
|
|
321
329
|
it("creates multiple datasource accessors", () => {
|
|
@@ -335,6 +343,10 @@ describe("Project Schema", () => {
|
|
|
335
343
|
expect(client.pageViews).toBeDefined();
|
|
336
344
|
expect(typeof client.events.append).toBe("function");
|
|
337
345
|
expect(typeof client.pageViews.append).toBe("function");
|
|
346
|
+
expect(typeof client.events.delete).toBe("function");
|
|
347
|
+
expect(typeof client.pageViews.delete).toBe("function");
|
|
348
|
+
expect(typeof client.events.truncate).toBe("function");
|
|
349
|
+
expect(typeof client.pageViews.truncate).toBe("function");
|
|
338
350
|
});
|
|
339
351
|
|
|
340
352
|
it("accepts devMode option", () => {
|
package/src/schema/project.ts
CHANGED
|
@@ -12,8 +12,12 @@ import type {
|
|
|
12
12
|
AppendOptions,
|
|
13
13
|
AppendResult,
|
|
14
14
|
DatasourcesNamespace,
|
|
15
|
+
DeleteOptions,
|
|
16
|
+
DeleteResult,
|
|
15
17
|
QueryOptions,
|
|
16
18
|
QueryResult,
|
|
19
|
+
TruncateOptions,
|
|
20
|
+
TruncateResult,
|
|
17
21
|
} from "../client/types.js";
|
|
18
22
|
import type { InferRow, InferParams, InferOutputRow } from "../infer/index.js";
|
|
19
23
|
import type { TokensNamespace } from "../client/tokens.js";
|
|
@@ -82,16 +86,20 @@ type IngestMethods<T extends DatasourcesDefinition> = {
|
|
|
82
86
|
};
|
|
83
87
|
|
|
84
88
|
/**
|
|
85
|
-
* Type for a datasource accessor with
|
|
89
|
+
* Type for a datasource accessor with import/mutation methods
|
|
86
90
|
*/
|
|
87
91
|
type DatasourceAccessor = {
|
|
88
92
|
/** Append data from a URL or file */
|
|
89
93
|
append(options: AppendOptions): Promise<AppendResult>;
|
|
94
|
+
/** Delete rows using a SQL condition */
|
|
95
|
+
delete(options: DeleteOptions): Promise<DeleteResult>;
|
|
96
|
+
/** Truncate all rows */
|
|
97
|
+
truncate(options?: TruncateOptions): Promise<TruncateResult>;
|
|
90
98
|
};
|
|
91
99
|
|
|
92
100
|
/**
|
|
93
101
|
* Type for datasource accessors object
|
|
94
|
-
* Maps each datasource to an accessor with
|
|
102
|
+
* Maps each datasource to an accessor with import/mutation methods
|
|
95
103
|
*/
|
|
96
104
|
type DatasourceAccessors<T extends DatasourcesDefinition> = {
|
|
97
105
|
[K in keyof T]: DatasourceAccessor;
|
|
@@ -105,7 +113,7 @@ interface ProjectClientBase<TDatasources extends DatasourcesDefinition> {
|
|
|
105
113
|
ingest: IngestMethods<TDatasources>;
|
|
106
114
|
/** Token operations (JWT creation, etc.) */
|
|
107
115
|
readonly tokens: TokensNamespace;
|
|
108
|
-
/** Datasource operations (append
|
|
116
|
+
/** Datasource operations (append/delete/truncate) */
|
|
109
117
|
readonly datasources: DatasourcesNamespace;
|
|
110
118
|
/** Execute raw SQL queries */
|
|
111
119
|
sql<T = unknown>(sql: string, options?: QueryOptions): Promise<QueryResult<T>>;
|
|
@@ -374,6 +382,14 @@ function buildProjectClient<
|
|
|
374
382
|
const client = await getClient();
|
|
375
383
|
return client.datasources.append(tinybirdName, options);
|
|
376
384
|
},
|
|
385
|
+
delete: async (options: DeleteOptions) => {
|
|
386
|
+
const client = await getClient();
|
|
387
|
+
return client.datasources.delete(tinybirdName, options);
|
|
388
|
+
},
|
|
389
|
+
truncate: async (options: TruncateOptions = {}) => {
|
|
390
|
+
const client = await getClient();
|
|
391
|
+
return client.datasources.truncate(tinybirdName, options);
|
|
392
|
+
},
|
|
377
393
|
};
|
|
378
394
|
}
|
|
379
395
|
|