@permute/sdk 0.3.0 → 0.5.0

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 CHANGED
@@ -26,7 +26,7 @@ The server validates source safety and derives the dashboard data contract from
26
26
  the referenced run. Clients cannot supply an authoritative contract.
27
27
 
28
28
  ```sh
29
- npm install @permute/sdk
29
+ bun add @permute/sdk
30
30
  ```
31
31
 
32
32
  ## Author data flows with external agents
@@ -101,6 +101,39 @@ and analyzing governed outputs; it does not expose code editing, draft inspectio
101
101
  execution, or publication. Hosted connectors also restrict access to selected sources. Flow scheduling and standalone operation
102
102
  creation are not part of this surface.
103
103
 
104
+ ## Publish write syncs
105
+
106
+ Use `client.writeSyncs.destinations()` to discover writable destination objects
107
+ and fields, then create a paused sync with `writeSyncs.create({ definition })`.
108
+ For operation sources, use `client.sources.get(operationId)` to inspect tables
109
+ and fields, then read `client.operations.get(operationId)` and use its
110
+ `published.revisionId` in the definition. Publish and run the operation before
111
+ configuring its sync. Definition edits use `writeSyncs.update(id, {
112
+ definition, expectedVersion })`, with the version from the latest read.
113
+
114
+ `writeSyncs.preview(id)` queues a read-only capture, including when the sync is
115
+ enabled. Poll `writeSyncs.getRun(id, runId)` until `previewComplete` or
116
+ `previewError`, then inspect every page with `getRun(id, runId, { page })`
117
+ (pages start at zero) before approving delivery.
118
+
119
+ ```ts
120
+ // After reviewing every page of a completed preview:
121
+ const history = await client.revisions.list(writeSyncId);
122
+ await client.revisions.publish(writeSyncId, {
123
+ expectedVersion: history.version,
124
+ runId,
125
+ });
126
+ ```
127
+
128
+ Publishing approves that preview, starts external delivery now, and schedules
129
+ new captures every 24 hours under the same API key that captured the preview.
130
+ New captures include `definitionRevisionId`, which identifies the exact saved
131
+ definition used by that run; older captures may omit it.
132
+ For an enabled sync, `writeSyncs.execute(id, runId)` sends a selected completed
133
+ preview captured by that same key without refreshing its values or changing the
134
+ schedule. Poll `getRun` for delivery results and inspect per-record outcomes.
135
+ Use `writeSyncs.pause(id)` to stop future writes; requests already sent may finish.
136
+
104
137
  ## Configure
105
138
 
106
139
  ```ts
@@ -314,24 +347,51 @@ const evidence = await client.search({
314
347
  console.log(evidence.items);
315
348
  ```
316
349
 
317
- Use SQL only when table names are known and exact rows matter. SQL requests must
318
- include `sourceIds`; call `client.sources.get()` first when table names are not
319
- known, and use the returned table names exactly. Warehouse-backed sources use
320
- DuckDB SQL and can be queried together. Live remote database connectors use
321
- their native dialect—GoogleSQL for BigQuery and PostgreSQL for Supabase—and
322
- must be queried one at a time without other sources.
350
+ Use SQL when table names are known and exact rows matter. SQL requests need
351
+ `sourceIds`. List a source's tables first, then describe the table you plan to
352
+ query (materialized connector fields come from the stored SQL table). Warehouse
353
+ sources use DuckDB SQL and can be queried together. Live remote sources use
354
+ their native SQL dialect and must be queried one at a time.
323
355
 
324
356
  ```ts
325
- const details = await client.sources.get(sourceIds[0]);
357
+ const dataset = sources.items.find((source) => source.type === 'dataset');
358
+ if (!dataset) throw new Error('No dataset is available');
359
+ const tables = await client.sources.listTables(dataset.id);
360
+ const table = await client.sources.describeTable(dataset.id, tables[0].id);
326
361
 
327
362
  const rows = await client.query({
328
- sourceIds: [details.source.id],
329
- sql: `SELECT * FROM "${details.tables[0].name}" LIMIT 10`,
363
+ sourceIds: [dataset.id],
364
+ sql: `SELECT * FROM "${table.id}" LIMIT 10`,
330
365
  });
331
366
 
332
- console.log(rows.rows);
367
+ console.log(table.fields, rows.rows, rows.timings);
333
368
  ```
334
369
 
370
+ `timings` separates source validation, DuckDB startup, source loading, and SQL
371
+ execution. Compare `totalMs` with the caller's elapsed time to find delay
372
+ outside the query action (including transport and response handling).
373
+
374
+ ## Managed databases
375
+
376
+ `client.databases` can create a database, define tables, read current records,
377
+ and submit atomic inserts, updates, or deletes. Use `client.query()` for filtered
378
+ SQL reads. Managed records include `__version`; updates and deletes need that
379
+ token, and a stale version returns 409. Reuse a `requestId` only when retrying
380
+ the same transaction after an uncertain response.
381
+
382
+ Schema definitions use `client.databases.definition(id)` and
383
+ `client.databases.saveDefinition(id, { definition, expectedDraft })`. Save the
384
+ complete table list, then publish the exact draft through `client.revisions`.
385
+ Published schemas only support adding tables; rows are not part of schema history.
386
+
387
+ **Upgrading from 0.3.x:** `createTable` now saves a draft. Publish the database
388
+ before using the new table. Direct `transact` calls require database admin
389
+ permission; use a business action for a delegated write.
390
+
391
+ See the [managed database guide](https://permute.ai/docs/guides/managed-database)
392
+ for a complete example (including provisioning, filtered reads, and conflict
393
+ handling).
394
+
335
395
  ## Error Handling
336
396
 
337
397
  ```ts
@@ -354,3 +414,216 @@ try {
354
414
  ## License
355
415
 
356
416
  Apache-2.0. Copyright 2026 Permute, Inc.
417
+
418
+ ## Hosted apps
419
+
420
+ Create an app with `client.apps.create({ name, key, description })`, then poll `client.apps.get(id)` until its deployment is live. Build your frontend locally with relative asset URLs (for Vite, `bun run build --base=./`).
421
+
422
+ ```typescript
423
+ const app = await client.apps.get(appId);
424
+ const files = await client.apps.uploadFiles(app.id, [
425
+ { path: 'index.html', data: await readFile('dist/index.html') },
426
+ { path: 'assets/main.js', data: await readFile('dist/assets/main.js') },
427
+ ]);
428
+ const saved = await client.apps.saveSite(app.id, {
429
+ site: { files, spa: true },
430
+ expectedDraft: app.draft,
431
+ });
432
+ const state = await client.revisions.list(app.id);
433
+ if (state.draft?.revisionId !== saved.draft.revisionId || state.draft.sequence !== saved.draft.sequence) {
434
+ throw new Error('The draft changed; review before publishing');
435
+ }
436
+ await client.revisions.publish(app.id, { expectedVersion: state.version });
437
+ ```
438
+
439
+ Import `readFile` from `node:fs/promises`. Supply the complete build directory; the filenames above are illustrative. Uploads hash the bytes and use signed, conditional S3 PUTs without forwarding API credentials. Uploading does not create a draft. `saveSite` verifies the uploaded files and saves the complete manifest as a draft revision; `revisions.publish` makes that draft live. The limits are 200 files, 4 MiB per file, and 50 MiB total.
440
+
441
+ Identical contents reuse existing files. Changed contents at the same path are stored separately, preserving earlier versions for rollback. Each save replaces the complete manifest (including unchanged files); omitted paths disappear from the live app when published. Resolve a 409 conflict against the current draft before retrying.
442
+
443
+ Visitors sign in through Permute and need app read permission. HTML receives a revision-specific base URL; use relative URLs for bundled and public assets. Inline scripts, external requests, workers, and server runtimes are not supported. Keep API keys and secrets out of compiled browser files. Restore a historical revision with `client.revisions.restore`, then publish the resulting draft to roll back. See the [complete app guide](https://permute.ai/docs/guides/deploy-app) for creation, directory uploads, updates, and rollback.
444
+
445
+ ### Deploy a build directory (SDK 0.5.0+)
446
+
447
+ The Node/Bun entry `@permute/sdk/deploy` combines validation, upload, draft save,
448
+ and publication for an existing live app. Available in SDK 0.5.0 and later; the
449
+ individual calls above remain available for separate draft review and publication.
450
+
451
+ ```ts
452
+ import { deployApp } from '@permute/sdk/deploy';
453
+
454
+ const deployed = await deployApp(client, { appId, directory: './dist' });
455
+ console.log(deployed.url, deployed.revisionId, deployed.changed);
456
+ ```
457
+
458
+ The helper rejects symbolic links, unsupported paths, missing `index.html`, and
459
+ oversized builds before uploading. It requires the current draft to be published;
460
+ after reviewing an existing draft, pass its exact `expectedDraft` to approve
461
+ replacing it. Omitted `actions` and `sourceIds` preserve current bindings (an empty
462
+ array clears them). To change action bindings, supply explicit published revisions:
463
+
464
+ ```ts
465
+ await deployApp(client, {
466
+ appId,
467
+ directory: './dist',
468
+ actions: [{ actionId, revisionId: publishedActionRevisionId }],
469
+ sourceIds: [databaseId],
470
+ });
471
+ ```
472
+
473
+ An unchanged build returns `changed: false` without uploading, saving, or publishing
474
+ (files, SPA mode, and any supplied bindings must match). It rechecks refs before
475
+ reporting no change. An approved unpublished draft is still published so settings
476
+ edits are preserved. Changed deployments return `changed: true`.
477
+
478
+ It stops if refs change and never retries mutations. After a failed or lost response,
479
+ inspect app history before retrying: uploads may exist, a draft may be saved, or
480
+ publication may already have completed. `deployment_conflict` requires review;
481
+ `invalid_build` requires fixing the local build. Use the individual calls above
482
+ when saving a draft for later review. The helper does not build, provision, or wait
483
+ for hosting, and must not be imported into browser code.
484
+
485
+ ### Inside a hosted app
486
+
487
+ Use the browser entry with the app's existing sign-in session (no API key in frontend code):
488
+
489
+ ```ts
490
+ import { PermuteAppClient } from '@permute/sdk/app';
491
+
492
+ const app = new PermuteAppClient();
493
+ const result = await app.query({
494
+ sourceIds: ['dbas_example'],
495
+ sql: 'SELECT * FROM app.contacts',
496
+ limit: 100,
497
+ });
498
+ const request = { requestId: crypto.randomUUID(), input: { name: 'Ada' } };
499
+ const receipt = await app.actions.invoke('actn_example', request);
500
+ ```
501
+
502
+ The client reads the hosted page's revision-pinned base URL. Save the app's `sourceIds` and
503
+ `actions: [{ actionId, revisionId }]` with its site manifest, then publish the app revision.
504
+ Queries require a declared source and the signed-in user's current read permission;
505
+ actions use the pinned published action revision and the user's invocation permission.
506
+ Read scope is source-level; client-supplied SQL is not a row-level authorization policy.
507
+ Keep the same request ID and input when retrying an uncertain action response. The client
508
+ does not retry writes automatically. A 401 requires signing in again.
509
+
510
+ For recovery across reloads, retain `{ baseUrl: document.baseURI, actionId, request }`
511
+ before sending (using storage appropriate to the input's sensitivity). After sign-in
512
+ or a newer app release, create `new PermuteAppClient({ baseUrl: pending.baseUrl })`
513
+ and invoke `pending.actionId` with `pending.request` as the same user. This keeps
514
+ the original app/action revision as well as the request ID and input. Never store
515
+ session cookies or API credentials with pending requests.
516
+
517
+ A later 401, 403, or 409 does not prove that an earlier attempt failed to commit.
518
+ Keep an uncertain request until its outcome is reconciled; do not silently clear
519
+ it, change its input, or generate a replacement ID. An edited payload is a new
520
+ operation only after resolving any earlier uncertain write.
521
+
522
+ ## Business actions
523
+
524
+ Use `client.actions.create`, `definition`, and `saveDefinition` to author a typed
525
+ handler with selected database tables or connector accounts. Publish with
526
+ `client.revisions.publish`, then call `client.actions.invoke` with a stable
527
+ `requestId` and input. `list` exposes published contracts; reading definitions
528
+ requires admin permission. See the [business action guide](https://permute.ai/docs/guides/business-actions)
529
+ for the complete create, save, publish, and invoke workflow.
530
+
531
+ ### Expected failures in handlers
532
+
533
+ Use `reject(code, message, details?)` for a deliberate business rejection:
534
+
535
+ ```js
536
+ exports.handler = ({ input, db, reject }) => {
537
+ if (!input.name.trim()) reject('validation_failed', 'Enter a name.', { field: 'name' });
538
+ db.insert('customers', input);
539
+ return input;
540
+ };
541
+ ```
542
+
543
+ Rejection stops the handler and discards staged database writes. It cannot undo
544
+ connector calls already sent. Messages and details are public to callers; never
545
+ put credentials or private diagnostic data in them. A rejection message permits
546
+ 1–1,000 characters, with up to 20 string detail fields (keys 1–100 characters,
547
+ values up to 500). Invalid rejections and unexpected exceptions return a safe
548
+ `execution_failed` response; exception text stays in server logs.
549
+
550
+ Both SDK entries expose `PermuteApiError.code`, `.status`, and `.details`.
551
+ Branch on the code (not message text):
552
+
553
+ | Code | HTTP | Recovery |
554
+ | -------------------------------- | -------- | --------------------------------------------------------------------- |
555
+ | `validation_failed` | 400 | Correct input; submit the changed input as a new request. |
556
+ | `record_conflict` | 409 | Reload current data and resolve the stale edit. |
557
+ | `invalid_transition` | 409 | Choose an allowed transition from the current state. |
558
+ | `permission_denied`, `not_found` | 403, 404 | Check current access or the selected record. |
559
+ | `request_conflict` | 409 | This ID already identifies different input; do not reuse it. |
560
+ | `request_in_progress` | 409 | Wait, then retry the identical request. |
561
+ | `outcome_unknown` | 409 | Retain the request ID; inspect external effects before any new write. |
562
+ | `execution_failed` | 500 | Inspect server logs; retry only with the same request ID and input. |
563
+
564
+ Handlers may reject with the first five codes. Transport/authentication failures
565
+ can still use the API's general error codes. A rejection after an external call
566
+ does not establish that nothing happened; reconcile provider state first.
567
+
568
+ ### Connector bindings
569
+
570
+ An action definition can bind multiple connector accounts by name alongside its database
571
+ schema bindings. Save and publish require admin permission on each selected resource.
572
+ The action runs with those delegated capabilities; callers need its invocation permission.
573
+
574
+ ```ts
575
+ await client.actions.saveDefinition(actionId, {
576
+ expectedDraft: null, // Use the current draft ref when updating.
577
+ definition: {
578
+ source: handlerSource,
579
+ inputSchema,
580
+ outputSchema,
581
+ invokePermission: 'write',
582
+ sources: databaseBindings,
583
+ connectors: {
584
+ salesCrm: 'conn_aaaaaaaaaaaaaaaaaaaaaaaa',
585
+ partnerCrm: 'conn_bbbbbbbbbbbbbbbbbbbbbbbb',
586
+ },
587
+ },
588
+ });
589
+ ```
590
+
591
+ Both names can refer to HubSpot accounts. Handler code uses the bound name; credentials
592
+ stay in the trusted proxy. The following handler creates a HubSpot deal and stages a local
593
+ record (the action must declare `crm_deals` in its writable database binding):
594
+
595
+ ```js
596
+ exports.handler = async ({ input, connectors, db }) => {
597
+ const deal = await connectors.salesCrm.request({
598
+ stepId: 'create-deal',
599
+ method: 'POST',
600
+ path: '/crm/v3/objects/deals',
601
+ body: {
602
+ properties: {
603
+ dealname: input.name,
604
+ dealstage: input.hubspotStageId,
605
+ pipeline: input.hubspotPipelineId,
606
+ },
607
+ },
608
+ });
609
+ db.insert('crm_deals', { id: input.id, name: input.name, hubspot_id: deal.id });
610
+ return { id: input.id, hubspotId: deal.id };
611
+ };
612
+ ```
613
+
614
+ `db.insert/update/delete` stages the local mutations until the handler finishes and its
615
+ output validates. Existing `{ mutations, output }` handlers remain supported. External
616
+ requests run when awaited and cannot be rolled back if the local commit later fails.
617
+
618
+ HTTP providers expose `request({ stepId, method, path, headers?, body?, service? })` using
619
+ provider-relative paths and current authentication. Supabase and BigQuery expose
620
+ `query(sql, params, { stepId })`; S3 exposes `putObject({ stepId, key, body, contentType? })`
621
+ for text objects in the configured bucket. The connected account's grants apply without
622
+ an action-level endpoint allowlist. The SEC integration has no action transport because
623
+ it uses a shared Permute service key.
624
+
625
+ Reuse the same invocation `requestId` and input after a failed or lost response, and keep
626
+ step IDs stable in code. The first attempt pins the revision; successful external steps
627
+ replay saved responses. Steps with uncertain outcomes block automatic resend and require
628
+ provider-state reconciliation. There is no automatic rollback, reconciliation API, or
629
+ long-running workflow runner. Connector-only actions can use `sources: []`.
package/dist/app.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { ActionRequest, ActionReceipt, QueryRequest, QueryResponse } from './types.js';
2
+ export { PermuteApiError } from './errors.js';
3
+ export type { ActionRequest, ActionReceipt, QueryRequest, QueryResponse } from './types.js';
4
+ export interface PermuteAppClientOptions {
5
+ /** Defaults to the hosted page's revision-pinned document.baseURI. */
6
+ baseUrl?: string;
7
+ fetch?: typeof fetch;
8
+ }
9
+ export interface AppRequestOptions {
10
+ signal?: AbortSignal;
11
+ }
12
+ /** Browser client authenticated by the hosted app's HttpOnly session cookie. */
13
+ export declare class PermuteAppClient {
14
+ private readonly baseUrl;
15
+ private readonly fetcher;
16
+ constructor(options?: PermuteAppClientOptions);
17
+ readonly actions: {
18
+ /** Keep requestId and input unchanged when retrying an uncertain result. */
19
+ invoke: (actionId: string, request: ActionRequest, options?: AppRequestOptions) => Promise<ActionReceipt>;
20
+ };
21
+ query(request: QueryRequest, options?: AppRequestOptions): Promise<QueryResponse>;
22
+ private request;
23
+ }
package/dist/app.js ADDED
@@ -0,0 +1,65 @@
1
+ import { PermuteApiError } from './errors.js';
2
+ export { PermuteApiError } from './errors.js';
3
+ /** Browser client authenticated by the hosted app's HttpOnly session cookie. */
4
+ export class PermuteAppClient {
5
+ baseUrl;
6
+ fetcher;
7
+ constructor(options = {}) {
8
+ const base = options.baseUrl ?? (typeof document !== 'undefined' ? document.baseURI : undefined);
9
+ if (!base)
10
+ throw new Error('A hosted app revision URL is required.');
11
+ const url = new URL(base);
12
+ const revision = /^\/__revisions\/(rvsn_[a-z0-9]+)\//.exec(url.pathname);
13
+ if (!revision || !['https:', 'http:'].includes(url.protocol) || url.username || url.password)
14
+ throw new Error('Use a revision-pinned hosted app URL.');
15
+ if (typeof location !== 'undefined' && url.origin !== location.origin)
16
+ throw new Error('App requests must use the current origin.');
17
+ this.baseUrl = `${url.origin}${revision[0]}`;
18
+ this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
19
+ }
20
+ actions = {
21
+ /** Keep requestId and input unchanged when retrying an uncertain result. */
22
+ invoke: (actionId, request, options) => this.request(`__actions/${encodeURIComponent(actionId)}/invoke`, request, options),
23
+ };
24
+ query(request, options) {
25
+ return this.request('__queries', request, options);
26
+ }
27
+ async request(path, body, options) {
28
+ const response = await this.fetcher(`${this.baseUrl}${path}`, {
29
+ method: 'POST',
30
+ credentials: 'same-origin',
31
+ mode: 'same-origin',
32
+ redirect: 'error',
33
+ headers: { 'Content-Type': 'application/json' },
34
+ body: JSON.stringify(body),
35
+ signal: options?.signal,
36
+ });
37
+ const text = await response.text();
38
+ let payload;
39
+ try {
40
+ payload = JSON.parse(text);
41
+ }
42
+ catch {
43
+ /* Gateways can return plain-text errors. */
44
+ }
45
+ if (!response.ok) {
46
+ const error = payload?.error;
47
+ throw new PermuteApiError({
48
+ status: response.status,
49
+ code: error?.code ?? (response.status === 401 ? 'unauthenticated' : 'request_failed'),
50
+ message: (typeof error === 'string' ? error : error?.message) ??
51
+ (response.headers.get('content-type')?.startsWith('text/plain') ? text : null) ??
52
+ `App request failed (${response.status}).`,
53
+ details: error?.details,
54
+ requestId: error?.requestId,
55
+ });
56
+ }
57
+ if (!payload || typeof payload !== 'object' || !Object.hasOwn(payload, 'data'))
58
+ throw new PermuteApiError({
59
+ status: response.status,
60
+ code: 'invalid_response',
61
+ message: 'Invalid app response.',
62
+ });
63
+ return payload.data;
64
+ }
65
+ }
package/dist/client.d.ts CHANGED
@@ -1,17 +1,6 @@
1
- import type { ConnectorSession, ConnectorSessionCreated, ConnectorSessionCreateRequest, Database, DatabaseList, DatabaseCreateRequest, DatabaseTables, DatabaseTableCreateRequest, DatabaseTableCreated, DatabaseRecords, DatabaseRecordsQuery, DatabaseTransactionRequest, DatabaseTransactionReceipt, ResourceVersionState, ResourceRevisionList, ResourceRevision, ResourceVersionRequest, Fact, FactList, FactWriteRequest, SkillList, SkillContent, SkillWriteRequest, KnowledgeWriteResponse, OperationRunGroup, OperationRunQuery, Operation, OperationList, OperationListQuery, OperationSource, OperationSourceUpdateRequest, OperationUpdateRequest, OperationRunRequest, OperationRunStarted, OperationRunList, OperationRunOutput, DataFlow, DataFlowCreateRequest, DataFlowCommandRequest, DataFlowCommandResponse, Workspace, WorkspaceCreateRequest, WorkspaceDeleteResponse, WorkspaceList, WorkspaceUpdateRequest, AnswerRequest, AnswerJobResponse, AnswerResponse, PermuteClientOptions, PermuteRequestOptions, QueryRequest, QueryResponse, SearchRequest, SearchResponse, SourceDetail, SourceList, CustomDatasetCreateRequest, DatasetDeleteResponse, DatasetResponse, FileDatasetCreateRequest, FileDatasetResponse, DatasetRows, CustomDatasetResponse, DashboardSource, DashboardSourceUpdateRequest } from './types.js';
2
- export declare class PermuteApiError extends Error {
3
- readonly status: number;
4
- readonly code: string;
5
- readonly details?: unknown;
6
- readonly requestId?: string;
7
- constructor(input: {
8
- status: number;
9
- code: string;
10
- message: string;
11
- details?: unknown;
12
- requestId?: string;
13
- });
14
- }
1
+ export { PermuteApiError } from './errors.js';
2
+ import type { BusinessAction, BusinessActionList, ActionCreateRequest, ActionDefinition, ActionDefinitionSaveRequest, ActionDefinitionSaved, ActionRequest, ActionReceipt } from './types.js';
3
+ import type { ConnectorSession, ConnectorSessionCreated, ConnectorSessionCreateRequest, Database, AppDraft, AppDraftList, AppCreateRequest, AppUpdateRequest, AppDeleted, AppUploadsRequest, AppUploads, AppSiteSaveRequest, AppUploadFile, DatabaseList, DatabaseCreateRequest, DatabaseDefinition, DatabaseDefinitionSaveRequest, DatabaseDefinitionSaved, DatabaseTables, DatabaseTableCreateRequest, DatabaseTableCreated, DatabaseRecords, DatabaseRecordsQuery, DatabaseTransactionRequest, DatabaseTransactionReceipt, WriteSync, WriteSyncCreateRequest, WriteSyncUpdateRequest, WriteSyncDetail, WriteSyncList, WriteSyncDeleted, WriteSyncDestinations, WriteSyncRun, WriteSyncRunList, WriteSyncRunQuery, WriteSyncExecution, ResourceVersionState, ResourceRevisionList, ResourceRevision, ResourceVersionRequest, ResourcePublishRequest, Fact, FactList, FactWriteRequest, SkillList, SkillContent, SkillWriteRequest, KnowledgeWriteResponse, OperationRunGroup, OperationRunQuery, Operation, OperationList, OperationListQuery, OperationSource, OperationSourceUpdateRequest, OperationUpdateRequest, OperationRunRequest, OperationRunStarted, OperationRunList, OperationRunOutput, DataFlow, DataFlowCreateRequest, DataFlowCommandRequest, DataFlowCommandResponse, Workspace, WorkspaceCreateRequest, WorkspaceDeleteResponse, WorkspaceList, WorkspaceUpdateRequest, AnswerRequest, AnswerJobResponse, AnswerResponse, PermuteClientOptions, PermuteRequestOptions, QueryRequest, QueryResponse, SearchRequest, SearchResponse, SourceDetail, CatalogTable, SourceList, CustomDatasetCreateRequest, DatasetDeleteResponse, DatasetResponse, FileDatasetCreateRequest, FileDatasetResponse, DatasetRows, CustomDatasetResponse, DashboardSource, DashboardSourceUpdateRequest } from './types.js';
15
4
  type DatasetWriteInput = {
16
5
  tables: Record<string, DatasetRows>;
17
6
  mode: 'replace';
@@ -71,7 +60,7 @@ export declare class PermuteClient {
71
60
  list(resourceId: string, options?: PermuteRequestOptions): Promise<ResourceRevisionList>;
72
61
  get(resourceId: string, revisionId: string, options?: PermuteRequestOptions): Promise<ResourceRevision>;
73
62
  restore(resourceId: string, revisionId: string, request: ResourceVersionRequest, options?: PermuteRequestOptions): Promise<ResourceVersionState>;
74
- publish(resourceId: string, request: ResourceVersionRequest, options?: PermuteRequestOptions): Promise<ResourceVersionState>;
63
+ publish(resourceId: string, request: ResourcePublishRequest, options?: PermuteRequestOptions): Promise<ResourceVersionState>;
75
64
  };
76
65
  readonly facts: {
77
66
  list(options?: PermuteRequestOptions): Promise<FactList>;
@@ -82,20 +71,54 @@ export declare class PermuteClient {
82
71
  };
83
72
  readonly skills: SkillsClient;
84
73
  readonly organizationSkills: SkillsClient;
74
+ readonly actions: {
75
+ list(options?: PermuteRequestOptions): Promise<BusinessActionList>;
76
+ create(body: ActionCreateRequest, options?: PermuteRequestOptions): Promise<BusinessAction>;
77
+ get(actionId: string, options?: PermuteRequestOptions): Promise<BusinessAction>;
78
+ delete(actionId: string, options?: PermuteRequestOptions): Promise<BusinessAction>;
79
+ definition(actionId: string, options?: PermuteRequestOptions): Promise<ActionDefinition>;
80
+ saveDefinition(actionId: string, body: ActionDefinitionSaveRequest, options?: PermuteRequestOptions): Promise<ActionDefinitionSaved>;
81
+ invoke(actionId: string, body: ActionRequest, options?: PermuteRequestOptions): Promise<ActionReceipt>;
82
+ };
85
83
  readonly databases: {
86
84
  list(options?: PermuteRequestOptions): Promise<DatabaseList>;
87
85
  create(body: DatabaseCreateRequest, options?: PermuteRequestOptions): Promise<Database>;
88
86
  get(databaseId: string, options?: PermuteRequestOptions): Promise<Database>;
89
87
  delete(databaseId: string, options?: PermuteRequestOptions): Promise<Database>;
88
+ definition(databaseId: string, options?: PermuteRequestOptions): Promise<DatabaseDefinition>;
89
+ saveDefinition(databaseId: string, body: DatabaseDefinitionSaveRequest, options?: PermuteRequestOptions): Promise<DatabaseDefinitionSaved>;
90
90
  tables(databaseId: string, options?: PermuteRequestOptions): Promise<DatabaseTables>;
91
91
  createTable(databaseId: string, body: DatabaseTableCreateRequest, options?: PermuteRequestOptions): Promise<DatabaseTableCreated>;
92
92
  records(databaseId: string, table: string, query?: Partial<DatabaseRecordsQuery>, options?: PermuteRequestOptions): Promise<DatabaseRecords>;
93
93
  transact(databaseId: string, body: DatabaseTransactionRequest, options?: PermuteRequestOptions): Promise<DatabaseTransactionReceipt>;
94
94
  };
95
+ readonly apps: {
96
+ list(options?: PermuteRequestOptions): Promise<AppDraftList>;
97
+ create(body: AppCreateRequest, options?: PermuteRequestOptions): Promise<AppDraft>;
98
+ get(applicationId: string, options?: PermuteRequestOptions): Promise<AppDraft>;
99
+ update(applicationId: string, body: AppUpdateRequest, options?: PermuteRequestOptions): Promise<AppDraft>;
100
+ delete(applicationId: string, options?: PermuteRequestOptions): Promise<AppDeleted>;
101
+ prepareUploads(applicationId: string, body: AppUploadsRequest, options?: PermuteRequestOptions): Promise<AppUploads>;
102
+ uploadFiles(applicationId: string, files: AppUploadFile[], options?: PermuteRequestOptions): Promise<AppUploadsRequest['files']>;
103
+ saveSite(applicationId: string, body: AppSiteSaveRequest, options?: PermuteRequestOptions): Promise<AppDraft>;
104
+ };
95
105
  readonly connectorSessions: {
96
106
  create(request: ConnectorSessionCreateRequest, options?: PermuteRequestOptions): Promise<ConnectorSessionCreated>;
97
107
  get(sessionId: string, options?: PermuteRequestOptions): Promise<ConnectorSession>;
98
108
  };
109
+ readonly writeSyncs: {
110
+ list(options?: PermuteRequestOptions): Promise<WriteSyncList>;
111
+ create(request: WriteSyncCreateRequest, options?: PermuteRequestOptions): Promise<WriteSync>;
112
+ get(writeSyncId: string, options?: PermuteRequestOptions): Promise<WriteSyncDetail>;
113
+ update(writeSyncId: string, request: WriteSyncUpdateRequest, options?: PermuteRequestOptions): Promise<WriteSync>;
114
+ delete(writeSyncId: string, options?: PermuteRequestOptions): Promise<WriteSyncDeleted>;
115
+ destinations(options?: PermuteRequestOptions): Promise<WriteSyncDestinations>;
116
+ preview(writeSyncId: string, options?: PermuteRequestOptions): Promise<WriteSyncRun>;
117
+ pause(writeSyncId: string, options?: PermuteRequestOptions): Promise<WriteSync>;
118
+ listRuns(writeSyncId: string, options?: PermuteRequestOptions): Promise<WriteSyncRunList>;
119
+ getRun(writeSyncId: string, runId: string, query?: WriteSyncRunQuery, options?: PermuteRequestOptions): Promise<WriteSyncRun>;
120
+ execute(writeSyncId: string, runId: string, options?: PermuteRequestOptions): Promise<WriteSyncExecution>;
121
+ };
99
122
  readonly workspaces: {
100
123
  list: (options?: PermuteRequestOptions) => Promise<WorkspaceList>;
101
124
  create: (request: WorkspaceCreateRequest, options?: PermuteRequestOptions) => Promise<Workspace>;
@@ -106,6 +129,8 @@ export declare class PermuteClient {
106
129
  readonly sources: {
107
130
  list: (options?: PermuteRequestOptions) => Promise<SourceList>;
108
131
  get: (sourceId: string, options?: PermuteRequestOptions) => Promise<SourceDetail>;
132
+ listTables: (sourceId: string, options?: PermuteRequestOptions) => Promise<CatalogTable[]>;
133
+ describeTable: (sourceId: string, tableId: string, options?: PermuteRequestOptions) => Promise<CatalogTable>;
109
134
  };
110
135
  readonly operations: {
111
136
  list: (query?: OperationListQuery, options?: PermuteRequestOptions) => Promise<OperationList>;
@@ -156,4 +181,3 @@ export declare class PermuteClient {
156
181
  private validateRequestBody;
157
182
  private validateRequestQuery;
158
183
  }
159
- export {};
package/dist/client.js CHANGED
@@ -1,22 +1,10 @@
1
+ import { PermuteApiError } from './errors.js';
2
+ export { PermuteApiError } from './errors.js';
1
3
  import { Ajv } from 'ajv';
2
4
  import { createHash } from 'node:crypto';
3
5
  import { publicApiRoutes } from './generated/runtime-contract.js';
4
6
  const DEFAULT_BASE_URL = 'https://api.permute.ai';
5
7
  const DEFAULT_POLL_TIMEOUT_MS = 20 * 60 * 1000;
6
- export class PermuteApiError extends Error {
7
- status;
8
- code;
9
- details;
10
- requestId;
11
- constructor(input) {
12
- super(input.message);
13
- this.name = 'PermuteApiError';
14
- this.status = input.status;
15
- this.code = input.code;
16
- this.details = input.details;
17
- this.requestId = input.requestId;
18
- }
19
- }
20
8
  function routeFor(operationId) {
21
9
  const route = publicApiRoutes[operationId];
22
10
  if (!route) {
@@ -266,8 +254,11 @@ export class PermuteClient {
266
254
  facts;
267
255
  skills;
268
256
  organizationSkills;
257
+ actions;
269
258
  databases;
259
+ apps;
270
260
  connectorSessions;
261
+ writeSyncs;
271
262
  workspaces;
272
263
  sources;
273
264
  operations;
@@ -322,20 +313,88 @@ export class PermuteClient {
322
313
  });
323
314
  this.skills = skillsClient('skills');
324
315
  this.organizationSkills = skillsClient('organizationSkills');
316
+ this.actions = {
317
+ list: (options) => this.request('actionsList', {}, options),
318
+ create: (body, options) => this.request('actionsCreate', { body }, options),
319
+ get: (actionId, options) => this.request('actionsGet', { pathParams: { actionId } }, options),
320
+ delete: (actionId, options) => this.request('actionsDelete', { pathParams: { actionId } }, options),
321
+ definition: (actionId, options) => this.request('actionDefinitionGet', { pathParams: { actionId } }, options),
322
+ saveDefinition: (actionId, body, options) => this.request('actionDefinitionSave', { pathParams: { actionId }, body }, options),
323
+ invoke: (actionId, body, options) => this.request('actionsInvoke', { pathParams: { actionId }, body }, options),
324
+ };
325
325
  this.databases = {
326
326
  list: (options) => this.request('databasesList', {}, options),
327
327
  create: (body, options) => this.request('databasesCreate', { body }, options),
328
328
  get: (databaseId, options) => this.request('databasesGet', { pathParams: { databaseId } }, options),
329
329
  delete: (databaseId, options) => this.request('databasesDelete', { pathParams: { databaseId } }, options),
330
+ definition: (databaseId, options) => this.request('databaseDefinitionGet', { pathParams: { databaseId } }, options),
331
+ saveDefinition: (databaseId, body, options) => this.request('databaseDefinitionSave', { pathParams: { databaseId }, body }, options),
330
332
  tables: (databaseId, options) => this.request('databaseTablesList', { pathParams: { databaseId } }, options),
331
333
  createTable: (databaseId, body, options) => this.request('databaseTablesCreate', { pathParams: { databaseId }, body }, options),
332
334
  records: (databaseId, table, query = {}, options) => this.request('databaseRecordsList', { pathParams: { databaseId, table }, query }, options),
333
335
  transact: (databaseId, body, options) => this.request('databaseTransactionsCreate', { pathParams: { databaseId }, body }, options),
334
336
  };
337
+ this.apps = {
338
+ prepareUploads: (applicationId, body, options) => this.request('appsUploadsCreate', { pathParams: { applicationId }, body }, options),
339
+ uploadFiles: async (applicationId, files, options) => {
340
+ const contents = await Promise.all(files.map(async (file) => ({
341
+ path: file.path,
342
+ bytes: typeof file.data === 'string'
343
+ ? Buffer.from(file.data)
344
+ : file.data instanceof Blob
345
+ ? Buffer.from(await file.data.arrayBuffer())
346
+ : Buffer.from(file.data),
347
+ })));
348
+ const manifest = contents.map(({ path, bytes }) => ({
349
+ path,
350
+ sha256: createHash('sha256').update(bytes).digest('hex'),
351
+ size: bytes.length,
352
+ }));
353
+ const { uploads } = await this.apps.prepareUploads(applicationId, { files: manifest }, options);
354
+ for (const target of uploads) {
355
+ const file = contents.find((item) => item.path === target.path);
356
+ if (!file)
357
+ throw new Error('Unexpected app upload path');
358
+ const uploaded = await this.fetchImpl(target.url, {
359
+ method: 'PUT',
360
+ headers: target.headers,
361
+ body: new Blob([new Uint8Array(file.bytes)]),
362
+ signal: options?.signal,
363
+ });
364
+ // A retry or another identical file can win the conditional PUT. Save verifies the stored checksum.
365
+ if (!uploaded.ok && uploaded.status !== 412)
366
+ throw new PermuteApiError({
367
+ status: uploaded.status,
368
+ code: 'upload_failed',
369
+ message: `App upload failed for ${target.path}`,
370
+ });
371
+ }
372
+ return manifest;
373
+ },
374
+ saveSite: (applicationId, body, options) => this.request('appsSiteUpdate', { pathParams: { applicationId }, body }, options),
375
+ list: (options) => this.request('appsList', {}, options),
376
+ create: (body, options) => this.request('appsCreate', { body }, options),
377
+ get: (applicationId, options) => this.request('appsGet', { pathParams: { applicationId } }, options),
378
+ update: (applicationId, body, options) => this.request('appsUpdate', { pathParams: { applicationId }, body }, options),
379
+ delete: (applicationId, options) => this.request('appsDelete', { pathParams: { applicationId } }, options),
380
+ };
335
381
  this.connectorSessions = {
336
382
  create: (body, options) => this.request('connectorSessionsCreate', { body }, options),
337
383
  get: (sessionId, options) => this.request('connectorSessionsGet', { pathParams: { sessionId } }, options),
338
384
  };
385
+ this.writeSyncs = {
386
+ list: (options) => this.request('writeSyncsList', {}, options),
387
+ create: (body, options) => this.request('writeSyncsCreate', { body }, options),
388
+ get: (writeSyncId, options) => this.request('writeSyncsGet', { pathParams: { writeSyncId } }, options),
389
+ update: (writeSyncId, body, options) => this.request('writeSyncsUpdate', { pathParams: { writeSyncId }, body }, options),
390
+ delete: (writeSyncId, options) => this.request('writeSyncsDelete', { pathParams: { writeSyncId } }, options),
391
+ destinations: (options) => this.request('writeSyncsDestinations', {}, options),
392
+ preview: (writeSyncId, options) => this.request('writeSyncsPreview', { pathParams: { writeSyncId } }, options),
393
+ pause: (writeSyncId, options) => this.request('writeSyncsPause', { pathParams: { writeSyncId } }, options),
394
+ listRuns: (writeSyncId, options) => this.request('writeSyncsListRuns', { pathParams: { writeSyncId } }, options),
395
+ getRun: (writeSyncId, runId, query = {}, options) => this.request('writeSyncsGetRun', { pathParams: { writeSyncId, runId }, query }, options),
396
+ execute: (writeSyncId, runId, options) => this.request('writeSyncsExecute', { pathParams: { writeSyncId, runId } }, options),
397
+ };
339
398
  this.workspaces = {
340
399
  list: (requestOptions) => this.request('workspacesList', {}, requestOptions),
341
400
  create: (request, requestOptions) => this.request('workspacesCreate', { body: request }, requestOptions),
@@ -346,6 +405,14 @@ export class PermuteClient {
346
405
  this.sources = {
347
406
  list: (requestOptions) => this.request('sourcesList', {}, requestOptions),
348
407
  get: (sourceId, requestOptions) => this.request('sourcesGet', { pathParams: { sourceId } }, requestOptions),
408
+ listTables: async (sourceId, requestOptions) => (await this.request('sourcesGet', {
409
+ pathParams: { sourceId },
410
+ query: { includeFields: 'false' },
411
+ }, requestOptions)).tables,
412
+ describeTable: async (sourceId, tableId, requestOptions) => (await this.request('sourcesGet', {
413
+ pathParams: { sourceId },
414
+ query: { tableId },
415
+ }, requestOptions)).tables[0],
349
416
  };
350
417
  this.operations = {
351
418
  list: (query = {}, options) => this.request('operationsList', { query }, options),