@tinybirdco/sdk 0.0.37 → 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.
Files changed (54) hide show
  1. package/README.md +94 -1
  2. package/dist/api/api.d.ts +67 -1
  3. package/dist/api/api.d.ts.map +1 -1
  4. package/dist/api/api.js +79 -0
  5. package/dist/api/api.js.map +1 -1
  6. package/dist/api/api.test.js +143 -0
  7. package/dist/api/api.test.js.map +1 -1
  8. package/dist/api/tokens.d.ts +79 -0
  9. package/dist/api/tokens.d.ts.map +1 -0
  10. package/dist/api/tokens.js +80 -0
  11. package/dist/api/tokens.js.map +1 -0
  12. package/dist/api/tokens.test.d.ts +2 -0
  13. package/dist/api/tokens.test.d.ts.map +1 -0
  14. package/dist/api/tokens.test.js +209 -0
  15. package/dist/api/tokens.test.js.map +1 -0
  16. package/dist/client/base.d.ts +19 -0
  17. package/dist/client/base.d.ts.map +1 -1
  18. package/dist/client/base.js +43 -0
  19. package/dist/client/base.js.map +1 -1
  20. package/dist/client/base.test.js +4 -0
  21. package/dist/client/base.test.js.map +1 -1
  22. package/dist/client/tokens.d.ts +42 -0
  23. package/dist/client/tokens.d.ts.map +1 -0
  24. package/dist/client/tokens.js +67 -0
  25. package/dist/client/tokens.js.map +1 -0
  26. package/dist/client/tokens.test.d.ts +2 -0
  27. package/dist/client/tokens.test.d.ts.map +1 -0
  28. package/dist/client/tokens.test.js +79 -0
  29. package/dist/client/tokens.test.js.map +1 -0
  30. package/dist/client/types.d.ts +50 -0
  31. package/dist/client/types.d.ts.map +1 -1
  32. package/dist/index.d.ts +4 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +2 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/schema/project.d.ts +27 -14
  37. package/dist/schema/project.d.ts.map +1 -1
  38. package/dist/schema/project.js +63 -13
  39. package/dist/schema/project.js.map +1 -1
  40. package/dist/schema/project.test.js +25 -12
  41. package/dist/schema/project.test.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/api/api.test.ts +208 -0
  44. package/src/api/api.ts +183 -0
  45. package/src/api/tokens.test.ts +253 -0
  46. package/src/api/tokens.ts +169 -0
  47. package/src/client/base.test.ts +4 -0
  48. package/src/client/base.ts +65 -0
  49. package/src/client/tokens.test.ts +103 -0
  50. package/src/client/tokens.ts +69 -0
  51. package/src/client/types.ts +54 -0
  52. package/src/index.ts +21 -0
  53. package/src/schema/project.test.ts +25 -12
  54. package/src/schema/project.ts +113 -29
@@ -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,7 +235,13 @@ export type {
231
235
  TinybirdApiQueryOptions,
232
236
  TinybirdApiIngestOptions,
233
237
  TinybirdApiAppendOptions,
238
+ TinybirdApiDeleteOptions,
239
+ TinybirdApiTruncateOptions,
234
240
  TinybirdApiRequestInit,
241
+ TinybirdApiTokenScope,
242
+ TinybirdApiCreateTokenRequest,
243
+ TinybirdApiCreateTokenOptions,
244
+ TinybirdApiCreateTokenResult,
235
245
  } from "./api/api.js";
236
246
 
237
247
  // ============ Preview Environment ============
@@ -251,6 +261,17 @@ export {
251
261
  } from "./api/dashboard.js";
252
262
  export type { RegionInfo } from "./api/dashboard.js";
253
263
 
264
+ // ============ Token API ============
265
+ export { createJWT, TokenApiError } from "./api/tokens.js";
266
+ export type {
267
+ TokenApiConfig,
268
+ JWTScope,
269
+ JWTScopeType,
270
+ JWTLimits,
271
+ CreateJWTOptions,
272
+ CreateJWTResult,
273
+ } from "./api/tokens.js";
274
+
254
275
  // ============ Config Types ============
255
276
  // Import from config-types.ts to avoid bundling esbuild in client code
256
277
  export type { TinybirdConfig, DevMode } from "./cli/config-types.js";
@@ -70,7 +70,7 @@ describe("Project Schema", () => {
70
70
  expect(project.pipes.topEvents).toBe(topEvents);
71
71
  });
72
72
 
73
- it("creates tinybird client with query and ingest methods", () => {
73
+ it("creates tinybird client with pipe accessors and ingest methods", () => {
74
74
  const events = defineDatasource("events", {
75
75
  schema: { id: t.string() },
76
76
  });
@@ -86,14 +86,15 @@ describe("Project Schema", () => {
86
86
  pipes: { topEvents },
87
87
  });
88
88
 
89
- expect(project.tinybird.query).toBeDefined();
90
89
  expect(project.tinybird.ingest).toBeDefined();
91
- expect(typeof project.tinybird.query.topEvents).toBe("function");
90
+ expect(typeof project.tinybird.topEvents.query).toBe("function");
92
91
  expect(typeof project.tinybird.ingest.events).toBe("function");
93
92
  expect(typeof project.tinybird.ingest.eventsBatch).toBe("function");
93
+ expect((project.tinybird as unknown as Record<string, unknown>).query).toBeUndefined();
94
+ expect((project.tinybird as unknown as Record<string, unknown>).pipes).toBeUndefined();
94
95
  });
95
96
 
96
- it("creates datasource accessors with append method", () => {
97
+ it("creates datasource accessors with append/delete/truncate methods", () => {
97
98
  const events = defineDatasource("events", {
98
99
  schema: { timestamp: t.dateTime() },
99
100
  });
@@ -104,6 +105,8 @@ describe("Project Schema", () => {
104
105
 
105
106
  expect(project.tinybird.events).toBeDefined();
106
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");
107
110
  });
108
111
 
109
112
  it("creates multiple datasource accessors", () => {
@@ -122,6 +125,10 @@ describe("Project Schema", () => {
122
125
  expect(project.tinybird.pageViews).toBeDefined();
123
126
  expect(typeof project.tinybird.events.append).toBe("function");
124
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");
125
132
  });
126
133
 
127
134
  it("throws error when accessing client before initialization", () => {
@@ -144,7 +151,7 @@ describe("Project Schema", () => {
144
151
  });
145
152
 
146
153
  // Cast to any since the type system expects params but stub throws regardless
147
- const queryFn = project.tinybird.query.internalPipe as () => Promise<unknown>;
154
+ const queryFn = project.tinybird.internalPipe.query as () => Promise<unknown>;
148
155
  await expect(queryFn()).rejects.toThrow(
149
156
  'Pipe "internalPipe" is not exposed as an endpoint'
150
157
  );
@@ -277,7 +284,7 @@ describe("Project Schema", () => {
277
284
  process.env.NODE_ENV = originalNodeEnv;
278
285
  });
279
286
 
280
- it("creates a client with query and ingest methods", () => {
287
+ it("creates a client with pipe accessors and ingest methods", () => {
281
288
  const events = defineDatasource("events", {
282
289
  schema: { id: t.string() },
283
290
  });
@@ -293,14 +300,17 @@ describe("Project Schema", () => {
293
300
  pipes: { topEvents },
294
301
  });
295
302
 
296
- expect(client.query).toBeDefined();
297
303
  expect(client.ingest).toBeDefined();
298
- expect(typeof client.query.topEvents).toBe("function");
304
+ expect(client.sql).toBeDefined();
305
+ expect(typeof client.topEvents.query).toBe("function");
299
306
  expect(typeof client.ingest.events).toBe("function");
300
307
  expect(typeof client.ingest.eventsBatch).toBe("function");
308
+ expect(typeof client.sql).toBe("function");
309
+ expect((client as unknown as Record<string, unknown>).query).toBeUndefined();
310
+ expect((client as unknown as Record<string, unknown>).pipes).toBeUndefined();
301
311
  });
302
312
 
303
- it("creates datasource accessors with append method", () => {
313
+ it("creates datasource accessors with append/delete/truncate methods", () => {
304
314
  const events = defineDatasource("events", {
305
315
  schema: { id: t.string() },
306
316
  });
@@ -312,6 +322,8 @@ describe("Project Schema", () => {
312
322
 
313
323
  expect(client.events).toBeDefined();
314
324
  expect(typeof client.events.append).toBe("function");
325
+ expect(typeof client.events.delete).toBe("function");
326
+ expect(typeof client.events.truncate).toBe("function");
315
327
  });
316
328
 
317
329
  it("creates multiple datasource accessors", () => {
@@ -331,6 +343,10 @@ describe("Project Schema", () => {
331
343
  expect(client.pageViews).toBeDefined();
332
344
  expect(typeof client.events.append).toBe("function");
333
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");
334
350
  });
335
351
 
336
352
  it("accepts devMode option", () => {
@@ -345,7 +361,6 @@ describe("Project Schema", () => {
345
361
  devMode: true,
346
362
  });
347
363
 
348
- expect(clientWithDevMode.query).toBeDefined();
349
364
  expect(clientWithDevMode.ingest).toBeDefined();
350
365
 
351
366
  const clientWithoutDevMode = createTinybirdClient({
@@ -354,7 +369,6 @@ describe("Project Schema", () => {
354
369
  devMode: false,
355
370
  });
356
371
 
357
- expect(clientWithoutDevMode.query).toBeDefined();
358
372
  expect(clientWithoutDevMode.ingest).toBeDefined();
359
373
  });
360
374
 
@@ -373,7 +387,6 @@ describe("Project Schema", () => {
373
387
  devMode: true,
374
388
  });
375
389
 
376
- expect(client.query).toBeDefined();
377
390
  expect(client.ingest).toBeDefined();
378
391
  });
379
392
 
@@ -8,8 +8,19 @@ import type { PipeDefinition, ParamsDefinition, OutputDefinition } from "./pipe.
8
8
  import type { ConnectionDefinition } from "./connection.js";
9
9
  import { getEndpointConfig } from "./pipe.js";
10
10
  import type { TinybirdClient } from "../client/base.js";
11
- import type { AppendOptions, AppendResult, QueryResult } from "../client/types.js";
11
+ import type {
12
+ AppendOptions,
13
+ AppendResult,
14
+ DatasourcesNamespace,
15
+ DeleteOptions,
16
+ DeleteResult,
17
+ QueryOptions,
18
+ QueryResult,
19
+ TruncateOptions,
20
+ TruncateResult,
21
+ } from "../client/types.js";
12
22
  import type { InferRow, InferParams, InferOutputRow } from "../infer/index.js";
23
+ import type { TokensNamespace } from "../client/tokens.js";
13
24
 
14
25
  // Symbol for brand typing - use Symbol.for() for global registry
15
26
  // This ensures the same symbol is used across module instances
@@ -41,11 +52,14 @@ type QueryMethod<T extends PipeDefinition<ParamsDefinition, OutputDefinition>> =
41
52
  : never;
42
53
 
43
54
  /**
44
- * Type for query methods object
45
- * Note: At runtime, only pipes with endpoint: true are included
55
+ * Type for pipe entity accessors object
56
+ * Note: At runtime, all declared pipes are included. Non-endpoint pipes throw
57
+ * when queried with a clear error message.
46
58
  */
47
- type QueryMethods<T extends PipesDefinition> = {
48
- [K in keyof T]: QueryMethod<T[K]>;
59
+ type PipeEntityAccessors<T extends PipesDefinition> = {
60
+ [K in keyof T]: {
61
+ query: QueryMethod<T[K]>;
62
+ };
49
63
  };
50
64
 
51
65
  /**
@@ -72,16 +86,20 @@ type IngestMethods<T extends DatasourcesDefinition> = {
72
86
  };
73
87
 
74
88
  /**
75
- * Type for a datasource accessor with append method
89
+ * Type for a datasource accessor with import/mutation methods
76
90
  */
77
91
  type DatasourceAccessor = {
78
92
  /** Append data from a URL or file */
79
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>;
80
98
  };
81
99
 
82
100
  /**
83
101
  * Type for datasource accessors object
84
- * Maps each datasource to an accessor with append method
102
+ * Maps each datasource to an accessor with import/mutation methods
85
103
  */
86
104
  type DatasourceAccessors<T extends DatasourcesDefinition> = {
87
105
  [K in keyof T]: DatasourceAccessor;
@@ -90,14 +108,15 @@ type DatasourceAccessors<T extends DatasourcesDefinition> = {
90
108
  /**
91
109
  * Base project client interface
92
110
  */
93
- interface ProjectClientBase<
94
- TDatasources extends DatasourcesDefinition,
95
- TPipes extends PipesDefinition
96
- > {
97
- /** Query endpoint pipes */
98
- query: QueryMethods<TPipes>;
111
+ interface ProjectClientBase<TDatasources extends DatasourcesDefinition> {
99
112
  /** Ingest events to datasources */
100
113
  ingest: IngestMethods<TDatasources>;
114
+ /** Token operations (JWT creation, etc.) */
115
+ readonly tokens: TokensNamespace;
116
+ /** Datasource operations (append/delete/truncate) */
117
+ readonly datasources: DatasourcesNamespace;
118
+ /** Execute raw SQL queries */
119
+ sql<T = unknown>(sql: string, options?: QueryOptions): Promise<QueryResult<T>>;
101
120
  /** Raw client for advanced usage */
102
121
  readonly client: TinybirdClient;
103
122
  }
@@ -109,7 +128,9 @@ interface ProjectClientBase<
109
128
  export type ProjectClient<
110
129
  TDatasources extends DatasourcesDefinition,
111
130
  TPipes extends PipesDefinition
112
- > = ProjectClientBase<TDatasources, TPipes> & DatasourceAccessors<TDatasources>;
131
+ > = ProjectClientBase<TDatasources> &
132
+ DatasourceAccessors<TDatasources> &
133
+ PipeEntityAccessors<TPipes>;
113
134
 
114
135
  /**
115
136
  * Configuration for createTinybirdClient
@@ -243,7 +264,7 @@ export function isProjectDefinition(value: unknown): value is ProjectDefinition
243
264
  /**
244
265
  * Build a typed Tinybird client from datasources and pipes
245
266
  *
246
- * This is an internal helper that builds query/ingest methods.
267
+ * This is an internal helper that builds pipe query and datasource ingest methods.
247
268
  */
248
269
  function buildProjectClient<
249
270
  TDatasources extends DatasourcesDefinition,
@@ -253,6 +274,14 @@ function buildProjectClient<
253
274
  pipes: TPipes,
254
275
  options?: { baseUrl?: string; token?: string; configDir?: string; devMode?: boolean }
255
276
  ): ProjectClient<TDatasources, TPipes> {
277
+ const RESERVED_CLIENT_NAMES = new Set([
278
+ "ingest",
279
+ "tokens",
280
+ "datasources",
281
+ "sql",
282
+ "client",
283
+ ]);
284
+
256
285
  // Lazy client initialization
257
286
  let _client: TinybirdClient | null = null;
258
287
 
@@ -276,33 +305,57 @@ function buildProjectClient<
276
305
  return _client;
277
306
  };
278
307
 
279
- // Build query methods for pipes
280
- const queryMethods: Record<string, (params?: unknown) => Promise<unknown>> = {};
308
+ // Build pipe accessors with query methods
309
+ const pipeAccessors: Record<string, { query: (params?: unknown) => Promise<unknown> }> = {};
281
310
  for (const [name, pipe] of Object.entries(pipes)) {
311
+ if (name in datasources) {
312
+ throw new Error(
313
+ `Name conflict in createTinybirdClient(): "${name}" is defined as both datasource and pipe. ` +
314
+ `Rename one of them to expose both as top-level client properties.`
315
+ );
316
+ }
317
+ if (RESERVED_CLIENT_NAMES.has(name)) {
318
+ throw new Error(
319
+ `Name conflict in createTinybirdClient(): "${name}" is reserved by the client API. ` +
320
+ `Rename this pipe to expose it as a top-level client property.`
321
+ );
322
+ }
323
+
282
324
  const endpointConfig = getEndpointConfig(pipe);
283
325
 
284
326
  if (!endpointConfig) {
285
327
  // Non-endpoint pipes get a stub that throws a clear error
286
- queryMethods[name] = async () => {
287
- throw new Error(
288
- `Pipe "${name}" is not exposed as an endpoint. ` +
289
- `Set "endpoint: true" in the pipe definition to enable querying.`
290
- );
328
+ pipeAccessors[name] = {
329
+ query: async () => {
330
+ throw new Error(
331
+ `Pipe "${name}" is not exposed as an endpoint. ` +
332
+ `Set "endpoint: true" in the pipe definition to enable querying.`
333
+ );
334
+ },
291
335
  };
292
336
  continue;
293
337
  }
294
338
 
295
339
  // Use the Tinybird pipe name (snake_case)
296
340
  const tinybirdName = pipe._name;
297
- queryMethods[name] = async (params?: unknown) => {
298
- const client = await getClient();
299
- return client.query(tinybirdName, (params ?? {}) as Record<string, unknown>);
341
+ pipeAccessors[name] = {
342
+ query: async (params?: unknown) => {
343
+ const client = await getClient();
344
+ return client.query(tinybirdName, (params ?? {}) as Record<string, unknown>);
345
+ },
300
346
  };
301
347
  }
302
348
 
303
349
  // Build ingest methods for datasources
304
350
  const ingestMethods: Record<string, (data: unknown) => Promise<void>> = {};
305
351
  for (const [name, datasource] of Object.entries(datasources)) {
352
+ if (RESERVED_CLIENT_NAMES.has(name)) {
353
+ throw new Error(
354
+ `Name conflict in createTinybirdClient(): "${name}" is reserved by the client API. ` +
355
+ `Rename this datasource to expose it as a top-level client property.`
356
+ );
357
+ }
358
+
306
359
  // Use the Tinybird datasource name (snake_case)
307
360
  const tinybirdName = datasource._name;
308
361
 
@@ -329,14 +382,44 @@ function buildProjectClient<
329
382
  const client = await getClient();
330
383
  return client.datasources.append(tinybirdName, options);
331
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
+ },
332
393
  };
333
394
  }
334
395
 
335
396
  // Create the typed client object
336
397
  return {
337
398
  ...datasourceAccessors,
338
- query: queryMethods,
399
+ ...pipeAccessors,
339
400
  ingest: ingestMethods,
401
+ sql: async <T = unknown>(sql: string, options: QueryOptions = {}) => {
402
+ const client = await getClient();
403
+ return client.sql<T>(sql, options);
404
+ },
405
+ get tokens(): TokensNamespace {
406
+ // Synchronous access - will throw if not initialized
407
+ if (!_client) {
408
+ throw new Error(
409
+ "Client not initialized. Call a query or ingest method first, or access client asynchronously."
410
+ );
411
+ }
412
+ return _client.tokens;
413
+ },
414
+ get datasources(): DatasourcesNamespace {
415
+ // Synchronous access - will throw if not initialized
416
+ if (!_client) {
417
+ throw new Error(
418
+ "Client not initialized. Call a query or ingest method first, or access client asynchronously."
419
+ );
420
+ }
421
+ return _client.datasources;
422
+ },
340
423
  get client(): TinybirdClient {
341
424
  // Synchronous client access - will throw if not initialized
342
425
  if (!_client) {
@@ -352,12 +435,13 @@ function buildProjectClient<
352
435
  /**
353
436
  * Create a typed Tinybird client
354
437
  *
355
- * Creates a client with typed query and ingest methods based on the provided
438
+ * Creates a client with typed pipe query and datasource ingest methods based on
439
+ * the provided
356
440
  * datasources and pipes. This is the recommended way to create a Tinybird client
357
441
  * when using the SDK's auto-generated client file.
358
442
  *
359
443
  * @param config - Client configuration with datasources and pipes
360
- * @returns A typed client with query and ingest methods
444
+ * @returns A typed client with pipe query and datasource ingest methods
361
445
  *
362
446
  * @example
363
447
  * ```ts
@@ -371,7 +455,7 @@ function buildProjectClient<
371
455
  * });
372
456
  *
373
457
  * // Query a pipe (fully typed)
374
- * const result = await tinybird.query.topPages({
458
+ * const result = await tinybird.topPages.query({
375
459
  * start_date: new Date('2024-01-01'),
376
460
  * end_date: new Date('2024-01-31'),
377
461
  * });