@permute/sdk 0.8.1 → 0.10.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
@@ -20,6 +20,18 @@ bunx @permute/sdk auth logout
20
20
 
21
21
  Choose an organization, optionally limit the connection to one workspace, then select resources and permissions to approve in Permute. The CLI stores renewable credentials in your operating system’s credential store (no plaintext fallback). Active logins have no fixed maximum lifetime and expire after 30 days without renewal; company sign-in rules can require earlier authentication. Logging out revokes access before removing the local credential. Signing in again revokes the previous stored CLI session.
22
22
 
23
+ To request a particular scope, add `--organization ID`, `--workspace ID`, or `--access-file PATH` to login. The JSON file uses the same resource/permission selections as API-key creation:
24
+
25
+ ```json
26
+ {
27
+ "organizationId": "org_...",
28
+ "workspaceId": "wksp_...",
29
+ "permissions": [{ "resourceId": "dbas_...", "resourceType": "database", "level": "read" }]
30
+ }
31
+ ```
32
+
33
+ An SDK login can supply the same object as `new OAuthLogin({ requestedAccess })` before calling `beginDevice()` or `beginBrowser(...)`. The browser prefills the request and permits only narrower selections. When available in the same browser, reauthentication recalls settings from a prior session. Each login still requires confirmation, and current resource permissions apply (saved selections create no resource grants). The browser stores only a non-secret session ID; it does not create another grant or change existing sessions. Logout and Connections disconnect individual logins.
34
+
23
35
  The SDK reuses credentials in memory between requests and reloads the OS store before renewal or when another process changes the saved login. The local auth directory holds only a lock and a non-secret change marker; tokens stay out of ordinary files. Current server permissions and revocation still apply to every request.
24
36
 
25
37
  The CLI prints the issuer and `permute-cli` entry name to stderr before accessing the OS store. macOS controls its own permission dialog, which may identify the Node or Bun process. SDK applications stay silent by default and can supply `onCredentialStoreAccess: (issuer) => { /* show a notice */ }` to `OAuthLogin`; the callback runs once before the default OS store is accessed and receives no credentials.
@@ -130,8 +142,9 @@ Data Flow and operation authoring are REST/SDK only. MCP does not expose their c
130
142
  editing, draft inspection, execution, or publication. Hosted MCP access uses
131
143
  selected sources or a hosted app's current grants and published action bindings,
132
144
  intersected with the user's permissions. App actions require confirmation in
133
- Permute; see the [MCP guide](https://permute.ai/docs/mcp). Flow scheduling and
134
- standalone operation creation are not part of this surface.
145
+ Permute; see the [MCP guide](https://permute.ai/docs/mcp). MCP does not expose flow
146
+ scheduling or standalone operation creation; schedule operations and flows with
147
+ `client.operations.update`.
135
148
 
136
149
  ## Publish write syncs
137
150
 
@@ -325,10 +338,8 @@ const dataset = await client.datasets.create({
325
338
  context: [Context.QUICKBOOKS],
326
339
  });
327
340
 
328
- await dataset.replace({
329
- orders: [{ id: 1 }, { id: 2 }],
330
- customers: [{ id: 10 }],
331
- });
341
+ await dataset.replaceTable('orders', [{ id: 1 }, { id: 2 }]);
342
+ await dataset.replaceTable('customers', [{ id: 10 }]);
332
343
 
333
344
  console.log(dataset.sourceId, dataset.version, dataset.data.tables);
334
345
  ```
@@ -337,15 +348,30 @@ Dataset `context` accepts string identifiers, with `Context` providing named
337
348
  constants for known contexts (such as `Context.QUICKBOOKS`). It is not restricted
338
349
  to the connector provider list. Use `Providers` when selecting a connector.
339
350
 
340
- `replace()` sends the complete dataset snapshot. Tables omitted from the call
341
- are removed, while tables with the same name keep their stable identity for
342
- reports and dashboards. Retrying an identical replacement is a no-op.
351
+ ### Add or replace one table
343
352
 
344
- Use `replaceTable()` when the supplied rows are the complete current contents
345
- of one table. Existing rows not supplied are removed; other tables are unchanged:
353
+ Use `replaceTable()` to add a new named table or replace the complete contents
354
+ of an existing table. Existing rows not supplied are removed; other tables stay
355
+ in the dataset. Await each write before starting the next write to that dataset:
346
356
 
347
357
  ```ts
348
358
  await dataset.replaceTable('orders', allOrders);
359
+ await dataset.replaceTable('customers', allCustomers);
360
+ // Both tables now belong to the same dataset.
361
+ ```
362
+
363
+ ### Replace the complete snapshot or append new rows
364
+
365
+ `replace()` sends the complete dataset snapshot. Tables omitted from the call
366
+ are removed, while tables with the same name keep their stable identity for
367
+ reports and dashboards. Retrying an identical replacement is a no-op:
368
+
369
+ ```ts
370
+ await dataset.replace({
371
+ orders: [{ id: 1 }, { id: 2 }],
372
+ customers: [{ id: 10 }],
373
+ });
374
+ // Only orders and customers remain in this dataset.
349
375
  ```
350
376
 
351
377
  Use `appendTable()` only when every supplied row is new and previously written
@@ -363,14 +389,68 @@ and use a new value for a new append. It prevents a retried request from being
363
389
  applied twice; reusing it with different rows fails. It does not remove duplicate
364
390
  rows sent in different batches.
365
391
 
366
- When source records can change or disappear, collect the source's current rows
367
- and use `replaceTable()` instead. The SDK serializes custom writes as chunked
368
- NDJSON, uploads them directly to a signed S3 target, and waits for processing;
369
- it does not build Parquet locally. The REST upload supports up to 1,000 tables and
370
- 1 GB, but the current SDK object helper encodes the supplied rows in memory, so
371
- callers should stay comfortably within their process memory. `appendTable()`
372
- accepts up to 10,000 rows per batch. Durable SDK operations stop polling after
373
- 20 minutes by default; pass `pollTimeoutMs` to set a different bound.
392
+ ### Upload a local CSV
393
+
394
+ With SDK 0.9.0 or later, pass a disk-backed Blob to the same `replaceTable()` method.
395
+ Use Node.js 24 or later with `openAsBlob`; the SDK hashes the file in small blocks,
396
+ uploads it to S3, and waits for processing without loading the whole file into
397
+ memory:
398
+
399
+ ```ts
400
+ import { openAsBlob } from 'node:fs';
401
+
402
+ await dataset.replaceTable('customers', await openAsBlob('./customers.csv'), {
403
+ format: 'csv',
404
+ });
405
+ ```
406
+
407
+ Supply comma-separated UTF-8 CSV with unique, nonempty column names in its first row.
408
+ Permute derives the row count and infers column types; an optional `rowCount`
409
+ checks an expected count. Keep the local file unchanged until its upload finishes.
410
+ Header-only CSV can create an empty table.
411
+
412
+ ### Upload a gzipped CSV
413
+
414
+ Pass an existing CSV.gz file with `compression: 'gzip'`. The SDK sends the file
415
+ as-is (it does not gzip a plain CSV when you set the option). Unpack ZIP archives
416
+ before importing individual table files:
417
+
418
+ ```ts
419
+ await dataset.replaceTable('order_history', await openAsBlob('./order_history.csv.gz'), {
420
+ format: 'csv',
421
+ compression: 'gzip',
422
+ });
423
+ ```
424
+
425
+ ### Import a large export
426
+
427
+ The row-array overload still serializes objects as NDJSON and holds both the
428
+ rows and encoded upload in memory. Use the Blob overload for large CSV exports.
429
+
430
+ Each uploaded file can be at most 5 GiB (5,368,709,120 bytes), with at most 10 GiB
431
+ (10,737,418,240 bytes) after decompression. The upload limit measures compressed
432
+ bytes when using gzip. Raw REST NDJSON writes also accept `compression: 'gzip'`;
433
+ `sizeBytes` and `contentHash` describe the exact uploaded bytes. Each complete
434
+ table replacement can contain at most 10,000,000 rows. A full `replace()` accepts
435
+ up to 1,000 tables within that same upload limit. The byte limit is per write,
436
+ so separate table writes can accumulate several GB in one dataset. Table-by-table
437
+ imports become visible as each write completes; they are not an atomic snapshot
438
+ of the whole export. New empty NDJSON tables require an existing schema.
439
+
440
+ For CSV exports with hundreds of tables, await each `replaceTable()` call while
441
+ reusing the same dataset key. Use a different key for each separate source export.
442
+ The guide includes [folder imports](https://permute.ai/docs/guides/custom-dataset#folder-of-tables),
443
+ [multiple datasets](https://permute.ai/docs/guides/custom-dataset#multiple-datasets), and
444
+ [signed REST uploads](https://permute.ai/docs/guides/custom-dataset#csv-export-to-s3).
445
+ Validate imported column names, counts, and types before loading the full export.
446
+ These limits apply to custom table writes; `type: 'file'` creates a separate file
447
+ dataset with its own upload limits.
448
+
449
+ `appendTable()` accepts up to 10,000 rows per batch. Repeated small appends are
450
+ inefficient for a large snapshot; upload its complete CSV with `replaceTable()`.
451
+ Durable SDK operations stop polling after 20 minutes by default; pass
452
+ `pollTimeoutMs` to change the client wait. A polling timeout does not prove the
453
+ server write failed; inspect its status before retrying.
374
454
 
375
455
  ## Recommended Flow
376
456
 
@@ -814,7 +894,8 @@ account’s access even when the caller has no connector access. Permute enforce
814
894
  declared execution capabilities and validates input/output schemas; the handler
815
895
  defines the operation exposed to callers.
816
896
 
817
- Handlers receive trusted `caller: { id, type }` (`type` is `user` or `apiKey`).
897
+ Handlers receive trusted `caller: { id, type }` (`type` is `user`, `apiKey`, or
898
+ `system`; a scheduled trigger run passes `{ type: 'system', id: '<triggerId>' }`).
818
899
  An action can use that identity and server-side business data to check or deny a
819
900
  request when its rules warrant it (such as an ownership rule or allowed state transition).
820
901
  Apply those checks before the affected reads or writes. They supplement the
@@ -929,6 +1010,49 @@ replay saved responses. Steps with uncertain outcomes block automatic resend and
929
1010
  provider-state reconciliation. There is no automatic rollback, reconciliation API, or
930
1011
  long-running workflow runner. Connector-only actions can use `sources: []`.
931
1012
 
1013
+ ### Schedule triggers
1014
+
1015
+ A trigger invokes a published action automatically. Schedule triggers use the same UTC
1016
+ schedule format as operations and a fixed input which must match the published input contract.
1017
+ Schedule values are whole numbers: `hourly { minute }`, `daily { hour, minute }`,
1018
+ `weekly { hour, minute, dayOfWeek }` (0 is Sunday), and `monthly { hour, minute, dayOfMonth }`.
1019
+ Use `dayOfMonth` 1–28 to run in every month.
1020
+
1021
+ ```ts
1022
+ const trigger = await client.actionTriggers.create('actn_example', {
1023
+ type: 'schedule',
1024
+ config: { schedule: { type: 'daily', hour: 9, minute: 0 }, input: { region: 'EMEA' } },
1025
+ });
1026
+ await client.actionTriggers.update('actn_example', trigger.id, { isEnabled: false });
1027
+ const { items } = await client.actionTriggers.runs('actn_example', trigger.id);
1028
+ ```
1029
+
1030
+ Trigger routes require API key authentication. A trigger has no owner and runs as the workspace
1031
+ system identity, so its runs do not recheck anyone's access. Instead, creating, editing, or enabling a
1032
+ trigger requires invocation permission and authority to delegate every capability which the action
1033
+ declares (SDK permissions, connectors, and sources), and publishing an action with an enabled trigger
1034
+ requires the publisher to be able to invoke the new revision. Every reader of the action can see its
1035
+ triggers, and invokers can change them. Scheduled runs pass `caller: { type: 'system', id: '<triggerId>' }`.
1036
+ Resources which a scheduled run creates through the SDK belong to the action owner (the user who
1037
+ created the action, or the user who created its API key); scheduled runs cannot create or delete
1038
+ hosted resources (managed databases and apps) or create connector sessions. Each run uses the action's current published revision and the
1039
+ request ID `<triggerId>_<slot epoch ms>`, so a retried slot never runs twice. Runs start within about
1040
+ five minutes of their slot. Missed slots run once, as the newest missed slot, and the skipped
1041
+ slots appear as a `businessAction.trigger.skipped` audit event. A deleted or
1042
+ unpublished action, or input which no longer matches the contract, disables the trigger and
1043
+ sets `disabledReason`; enable it again with `update` after fixing the cause. A slot which cannot
1044
+ start for 24 hours, for example because of a persistent platform error, also disables the trigger.
1045
+ Publishing a new action revision does not check trigger input; an incompatible trigger turns
1046
+ off at its next run. A disable records no run, so read `isEnabled` and `disabledReason`; `nextRunAt`
1047
+ is `null` while the trigger is off. Updating `config` replaces the whole config and repeats the
1048
+ access and input checks, like enabling. Pausing with `isEnabled: false` keeps `disabledReason` `null`.
1049
+ `runs` returns the newest 50 runs with status, revision, and failure, but not their output; to keep a
1050
+ run's results, write them inside the handler. Saved receipts belong to their caller, so invoking with a
1051
+ run's request ID runs the action again.
1052
+ Failures inside the action do not disable the trigger and are not retried; the next slot runs as
1053
+ scheduled. They appear in `runs`, as `lastRun` on the trigger, and as a
1054
+ `businessAction.trigger.failed` audit event.
1055
+
932
1056
  ## Hosted app visitor access (next release)
933
1057
 
934
1058
  App admins can read `client.apps.access(appId)` and update access with
@@ -1,3 +1,5 @@
1
+ import { type RequestedAccess } from './request.js';
2
+ export type { RequestedAccess } from './request.js';
1
3
  import { PermuteClient } from '../client.js';
2
4
  import { type CredentialStore, type LoginCredentials } from './store.js';
3
5
  export { SystemCredentialStore, type CredentialStore, type LoginCredentials } from './store.js';
@@ -6,6 +8,7 @@ export declare const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_cod
6
8
  export interface OAuthOptions {
7
9
  issuer?: string;
8
10
  apiUrl?: string;
11
+ requestedAccess?: RequestedAccess;
9
12
  store?: CredentialStore;
10
13
  fetch?: typeof fetch;
11
14
  /** Called once before the default OS credential store is accessed; no tokens are passed. */
@@ -16,6 +19,7 @@ export declare class OAuthLogin {
16
19
  readonly apiUrl: string;
17
20
  private readonly store;
18
21
  private readonly fetchImpl;
22
+ private readonly requested;
19
23
  constructor(options?: OAuthOptions);
20
24
  private endpoint;
21
25
  private post;
@@ -1,3 +1,4 @@
1
+ import { authorizationDetails } from './request.js';
1
2
  import { z } from 'zod';
2
3
  import { createHash, randomBytes } from 'node:crypto';
3
4
  import { setTimeout } from 'node:timers/promises';
@@ -27,11 +28,13 @@ export class OAuthLogin {
27
28
  apiUrl;
28
29
  store;
29
30
  fetchImpl;
31
+ requested;
30
32
  constructor(options = {}) {
31
33
  this.issuer = this.endpoint(options.issuer ?? DEFAULT_ISSUER);
32
34
  this.apiUrl = this.endpoint(options.apiUrl ?? 'https://api.permute.ai');
33
35
  this.store = options.store ?? new SystemCredentialStore(this.issuer, options.onCredentialStoreAccess);
34
36
  this.fetchImpl = options.fetch ?? fetch;
37
+ this.requested = authorizationDetails(options.requestedAccess);
35
38
  }
36
39
  endpoint(value) {
37
40
  const url = new URL(value);
@@ -90,7 +93,7 @@ export class OAuthLogin {
90
93
  return credentials;
91
94
  }
92
95
  async beginDevice() {
93
- const parsed = DeviceSchema.safeParse(await this.post('/cli/device/authorization', { client_id: CLIENT_ID, scope: 'workspace' }));
96
+ const parsed = DeviceSchema.safeParse(await this.post('/cli/device/authorization', { client_id: CLIENT_ID, scope: 'workspace', ...this.requested }));
94
97
  if (!parsed.success)
95
98
  throw new Error('Invalid device login response');
96
99
  const result = parsed.data;
@@ -139,6 +142,7 @@ export class OAuthLogin {
139
142
  url.search = new URLSearchParams({
140
143
  client_id: CLIENT_ID,
141
144
  scope: 'workspace',
145
+ ...this.requested,
142
146
  response_type: 'code',
143
147
  redirect_uri: redirectURI,
144
148
  state,
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+ import type { components } from '../generated/openapi.js';
3
+ export type RequestedAccess = components['schemas']['OAuthRequestedAccess'];
4
+ export declare const RequestedAccessSchema: z.ZodCustom<{
5
+ organizationId?: string;
6
+ permissions?: {
7
+ level: 'read' | 'write' | 'admin';
8
+ operations?: ('application.read_metadata' | 'application.update_metadata' | 'application.read_implementation' | 'application.update_draft' | 'application.publish' | 'application.manage_access' | 'application.manage_hosting' | 'application.delete' | 'business_action.read_metadata' | 'business_action.invoke' | 'business_action.read_implementation' | 'business_action.update_draft' | 'business_action.publish' | 'business_action.manage_access' | 'business_action.delegate_access' | 'business_action.delete')[];
9
+ resourceId: string;
10
+ resourceType: string;
11
+ }[];
12
+ workspaceId?: string;
13
+ }, {
14
+ organizationId?: string;
15
+ permissions?: {
16
+ level: 'read' | 'write' | 'admin';
17
+ operations?: ('application.read_metadata' | 'application.update_metadata' | 'application.read_implementation' | 'application.update_draft' | 'application.publish' | 'application.manage_access' | 'application.manage_hosting' | 'application.delete' | 'business_action.read_metadata' | 'business_action.invoke' | 'business_action.read_implementation' | 'business_action.update_draft' | 'business_action.publish' | 'business_action.manage_access' | 'business_action.delegate_access' | 'business_action.delete')[];
18
+ resourceId: string;
19
+ resourceType: string;
20
+ }[];
21
+ workspaceId?: string;
22
+ }>;
23
+ /** RFC 9396 carries structured permissions without inventing a second operation catalog. */
24
+ export declare function authorizationDetails(request: RequestedAccess | undefined): Record<string, string>;
@@ -0,0 +1,15 @@
1
+ import { Ajv } from 'ajv';
2
+ import { z } from 'zod';
3
+ import { authorizationRequestSchema } from '../generated/runtime-contract.js';
4
+ const validate = new Ajv({ strict: false }).compile(authorizationRequestSchema);
5
+ export const RequestedAccessSchema = z.custom((value) => validate(value), 'Invalid requested access');
6
+ /** RFC 9396 carries structured permissions without inventing a second operation catalog. */
7
+ export function authorizationDetails(request) {
8
+ if (!request)
9
+ return {};
10
+ return {
11
+ authorization_details: JSON.stringify([
12
+ { type: 'https://permute.ai/permissions', ...RequestedAccessSchema.parse(request) },
13
+ ]),
14
+ };
15
+ }
package/dist/cli.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
3
+ import { RequestedAccessSchema } from './auth/request.js';
2
4
  import { createServer } from 'node:http';
3
5
  import { spawn } from 'node:child_process';
4
6
  import { parseArgs } from 'node:util';
@@ -75,6 +77,9 @@ export async function runCli(args = process.argv.slice(2)) {
75
77
  allowPositionals: true,
76
78
  options: {
77
79
  issuer: { type: 'string' },
80
+ 'access-file': { type: 'string' },
81
+ organization: { type: 'string' },
82
+ workspace: { type: 'string' },
78
83
  'api-url': { type: 'string' },
79
84
  'device-code': { type: 'boolean' },
80
85
  'no-browser': { type: 'boolean' },
@@ -83,14 +88,21 @@ export async function runCli(args = process.argv.slice(2)) {
83
88
  });
84
89
  const [area, command] = positionals;
85
90
  if (values.help || area !== 'auth' || !['login', 'logout', 'status'].includes(command ?? '')) {
86
- console.log('permute auth login [--device-code] [--no-browser]\npermute auth status\npermute auth logout\n\nUse --issuer and --api-url together for a development environment.');
91
+ console.log('permute auth login [--device-code] [--no-browser] [--organization ID] [--workspace ID] [--access-file PATH]\npermute auth status\npermute auth logout\n\nUse --issuer and --api-url together for a development environment.');
87
92
  if (!values.help)
88
93
  process.exitCode = 1;
89
94
  return;
90
95
  }
91
96
  if (!!values.issuer !== !!values['api-url'])
92
97
  throw new Error('Provide both --issuer and --api-url for another environment.');
98
+ const requestedAccess = {
99
+ ...(values['access-file'] &&
100
+ RequestedAccessSchema.parse(JSON.parse(await readFile(values['access-file'], 'utf8')))),
101
+ ...(values.organization && { organizationId: values.organization }),
102
+ ...(values.workspace && { workspaceId: values.workspace }),
103
+ };
93
104
  const login = new OAuthLogin({
105
+ ...(Object.keys(requestedAccess).length > 0 && { requestedAccess }),
94
106
  issuer: values.issuer,
95
107
  apiUrl: values['api-url'],
96
108
  onCredentialStoreAccess: (issuer) => {
package/dist/client.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { EmailSender, EmailSendRequest, EmailReceipt, PermissionGrant, PermissionGrantInput, PermissionGrantUpdate, PermissionGrantQuery, PermissionGrantList, PermissionGrantRevoked, PermissionInspectionInput, PermissionInspection, AccessRequest, AccessRequestInput, AccessRequestQuery, AccessRequestList, AccessRequestDecision } from './types.js';
1
+ import type { EmailSender, EmailSendRequest, EmailReceipt, PermissionGrant, PermissionGrantInput, PermissionGrantUpdate, PermissionGrantQuery, PermissionGrantList, PermissionGrantRevoked, PermissionInspectionInput, PermissionInspection, AccessRequest, AccessRequestInput, AccessRequestQuery, AccessRequestList, AccessRequestDecision, MemberList, OrganizationMemberInvite, WorkspaceMemberInvite, OrganizationMemberRoleUpdate, WorkspaceMemberRoleUpdate, MemberInvitation, MemberInvitationDelivery, MemberRoleUpdated, MemberRemoved, Organization, OrganizationList } from './types.js';
2
2
  export { PermuteApiError } from './errors.js';
3
- import type { BusinessAction, BusinessActionDetail, BusinessActionList, ActionCreateRequest, ActionDefinition, ActionDefinitionSaveRequest, ActionDefinitionSaved, ActionRequest, ActionReceipt, ActionPublicationRequest, ActionPublicationRequestInput, ActionPublicationRequestList, ActionPublicationDecision } from './types.js';
4
- import type { ConnectorSession, ConnectorSessionCreated, ConnectorSessionCreateRequest, Database, AppPublicationRequest, AppPublicationRequestInput, AppPublicationRequestList, AppPublicationDecision, AppDraft, AppAccess, AppAddressSettings, AppAddressSetRequest, AppAccessSetRequest, AppDraftList, AppCreateRequest, AppUpdateRequest, AppDeleted, AppUploadsRequest, AppUploads, AppSiteSaveRequest, AppSourceFileQuery, AppSourceFileContent, AppGrants, AppGrantSetRequest, 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, AnswerResponse, PermuteClientOptions, PermuteRequestOptions, QueryRequest, SearchRequest, SourceDetail, CatalogTable, SourceList, CustomDatasetCreateRequest, DatasetDeleteResponse, DatasetResponse, FileDatasetCreateRequest, FileDatasetMetadataCreateRequest, FileDatasetResponse, DatasetRows, CustomDatasetResponse, DashboardSource, DashboardSourceUpdateRequest } from './types.js';
3
+ import type { BusinessAction, BusinessActionDetail, BusinessActionList, ActionCreateRequest, ActionDefinition, ActionDefinitionSaveRequest, ActionDefinitionSaved, ActionRequest, ActionReceipt, ActionTrigger, ActionTriggerList, ActionTriggerCreateRequest, ActionTriggerUpdateRequest, ActionTriggerRunList, ActionPublicationRequest, ActionPublicationRequestInput, ActionPublicationRequestList, ActionPublicationDecision } from './types.js';
4
+ import type { ConnectorSession, ConnectorSessionCreated, ConnectorSessionCreateRequest, Database, AppPublicationRequest, AppPublicationRequestInput, AppPublicationRequestList, AppPublicationDecision, AppDraft, AppAccess, AppAddressSettings, AppAddressSetRequest, AppAccessSetRequest, AppDraftList, AppCreateRequest, AppUpdateRequest, AppDeleted, AppUploadsRequest, AppUploads, AppSiteSaveRequest, AppSourceFileQuery, AppSourceFileContent, AppGrants, AppGrantSetRequest, 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, AnswerResponse, PermuteClientOptions, PermuteRequestOptions, QueryRequest, SearchRequest, SourceDetail, CatalogTable, SourceList, CustomDatasetCreateRequest, DatasetDeleteResponse, DatasetResponse, FileDatasetCreateRequest, FileDatasetMetadataCreateRequest, FileDatasetResponse, DatasetRows, DatasetCsvUploadOptions, CustomDatasetResponse, DashboardSource, DashboardSourceUpdateRequest } from './types.js';
5
5
  type DatasetWriteInput = {
6
6
  tables: Record<string, DatasetRows>;
7
7
  mode: 'replace';
@@ -14,7 +14,11 @@ type DatasetWriteInput = {
14
14
  rows: DatasetRows;
15
15
  batchId: string;
16
16
  mode: 'appendTable';
17
- };
17
+ } | ({
18
+ tableName: string;
19
+ file: Blob;
20
+ mode: 'replaceTable';
21
+ } & DatasetCsvUploadOptions);
18
22
  type DatasetWriter = (id: string, request: DatasetWriteInput, options?: PermuteRequestOptions) => Promise<CustomDatasetResponse>;
19
23
  type DatasetReader = (id: string, options?: PermuteRequestOptions) => Promise<DatasetResponse>;
20
24
  export declare class PermuteDataset {
@@ -33,6 +37,7 @@ export declare class PermuteDataset {
33
37
  get version(): number;
34
38
  replace(tables: Record<string, DatasetRows>, options?: PermuteRequestOptions): Promise<this>;
35
39
  replaceTable(tableName: string, rows: DatasetRows, options?: PermuteRequestOptions): Promise<this>;
40
+ replaceTable(tableName: string, file: Blob, options: DatasetCsvUploadOptions & PermuteRequestOptions): Promise<this>;
36
41
  appendTable(tableName: string, rows: DatasetRows, options: {
37
42
  batchId: string;
38
43
  } & PermuteRequestOptions): Promise<this>;
@@ -57,6 +62,24 @@ export interface SkillsClient {
57
62
  delete(path: string, options?: PermuteRequestOptions): Promise<KnowledgeWriteResponse>;
58
63
  }
59
64
  export declare class PermuteClient {
65
+ readonly organizations: {
66
+ list(options?: PermuteRequestOptions): Promise<OrganizationList>;
67
+ get(organizationId: string, options?: PermuteRequestOptions): Promise<Organization>;
68
+ };
69
+ readonly organizationMembers: {
70
+ list(organizationId: string, options?: PermuteRequestOptions): Promise<MemberList>;
71
+ invite(organizationId: string, body: OrganizationMemberInvite, options?: PermuteRequestOptions): Promise<MemberInvitation>;
72
+ resendInvitation(organizationId: string, userId: string, options?: PermuteRequestOptions): Promise<MemberInvitationDelivery>;
73
+ updateRole(organizationId: string, userId: string, body: OrganizationMemberRoleUpdate, options?: PermuteRequestOptions): Promise<MemberRoleUpdated>;
74
+ remove(organizationId: string, userId: string, options?: PermuteRequestOptions): Promise<MemberRemoved>;
75
+ };
76
+ readonly workspaceMembers: {
77
+ list(workspaceId: string, options?: PermuteRequestOptions): Promise<MemberList>;
78
+ invite(workspaceId: string, body: WorkspaceMemberInvite, options?: PermuteRequestOptions): Promise<MemberInvitation>;
79
+ resendInvitation(workspaceId: string, userId: string, options?: PermuteRequestOptions): Promise<MemberInvitationDelivery>;
80
+ updateRole(workspaceId: string, userId: string, body: WorkspaceMemberRoleUpdate, options?: PermuteRequestOptions): Promise<MemberRoleUpdated>;
81
+ remove(workspaceId: string, userId: string, options?: PermuteRequestOptions): Promise<MemberRemoved>;
82
+ };
60
83
  readonly permissions: {
61
84
  update(permissionId: string, body: PermissionGrantUpdate, options?: PermuteRequestOptions): Promise<PermissionGrant>;
62
85
  list(query?: PermissionGrantQuery, options?: PermuteRequestOptions): Promise<PermissionGrantList>;
@@ -97,6 +120,14 @@ export declare class PermuteClient {
97
120
  saveDefinition(actionId: string, body: ActionDefinitionSaveRequest, options?: PermuteRequestOptions): Promise<ActionDefinitionSaved>;
98
121
  invoke(actionId: string, body: ActionRequest, options?: PermuteRequestOptions): Promise<ActionReceipt>;
99
122
  };
123
+ readonly actionTriggers: {
124
+ list(actionId: string, options?: PermuteRequestOptions): Promise<ActionTriggerList>;
125
+ create(actionId: string, body: ActionTriggerCreateRequest, options?: PermuteRequestOptions): Promise<ActionTrigger>;
126
+ get(actionId: string, triggerId: string, options?: PermuteRequestOptions): Promise<ActionTrigger>;
127
+ update(actionId: string, triggerId: string, body: ActionTriggerUpdateRequest, options?: PermuteRequestOptions): Promise<ActionTrigger>;
128
+ delete(actionId: string, triggerId: string, options?: PermuteRequestOptions): Promise<ActionTrigger>;
129
+ runs(actionId: string, triggerId: string, options?: PermuteRequestOptions): Promise<ActionTriggerRunList>;
130
+ };
100
131
  readonly emails: {
101
132
  get(options?: PermuteRequestOptions): Promise<EmailSender>;
102
133
  /** Only callable from Business Actions through platform.withStep(stepId). */
package/dist/client.js CHANGED
@@ -93,12 +93,45 @@ function inferUploadSize(data) {
93
93
  return undefined;
94
94
  }
95
95
  const DATASET_WRITE_CHUNK_ROWS = 500;
96
+ const DATASET_WRITE_CHUNK_BYTES = 16 * 1024 * 1024;
97
+ async function hashDatasetFile(file, signal) {
98
+ const hash = createHash('sha256');
99
+ const reader = file.stream().getReader();
100
+ try {
101
+ while (true) {
102
+ if (signal?.aborted) {
103
+ await reader.cancel();
104
+ throw new PermuteApiError({ status: 0, code: 'request_aborted', message: 'Request was aborted' });
105
+ }
106
+ const { done, value } = await reader.read();
107
+ if (done)
108
+ return hash.digest('hex');
109
+ hash.update(value);
110
+ }
111
+ }
112
+ finally {
113
+ reader.releaseLock();
114
+ }
115
+ }
96
116
  function encodeDatasetWrite(request) {
97
117
  const entries = request.mode === 'replace' ? Object.entries(request.tables) : [[request.tableName, request.rows]];
98
118
  const parts = [];
99
119
  const hash = createHash('sha256');
100
120
  const addChunk = (tableName, rows) => {
101
121
  const line = `${JSON.stringify({ tableName, rows })}\n`;
122
+ if (new TextEncoder().encode(line).byteLength > DATASET_WRITE_CHUNK_BYTES) {
123
+ if (rows.length <= 1) {
124
+ throw new PermuteApiError({
125
+ status: 0,
126
+ code: 'dataset_row_too_large',
127
+ message: 'A dataset row exceeds the 16 MiB upload chunk limit',
128
+ });
129
+ }
130
+ const middle = Math.floor(rows.length / 2);
131
+ addChunk(tableName, rows.slice(0, middle));
132
+ addChunk(tableName, rows.slice(middle));
133
+ return;
134
+ }
102
135
  parts.push(line);
103
136
  hash.update(line);
104
137
  };
@@ -113,7 +146,6 @@ function encodeDatasetWrite(request) {
113
146
  return {
114
147
  data,
115
148
  contentHash: hash.digest('hex'),
116
- tables: entries.map(([name, rows]) => ({ name, rowCount: rows.length })),
117
149
  };
118
150
  }
119
151
  function pollingDeadline(options) {
@@ -208,8 +240,20 @@ export class PermuteDataset {
208
240
  this.snapshot = await this.writeTables(this.id, { tables, mode: 'replace' }, options);
209
241
  return this;
210
242
  }
211
- async replaceTable(tableName, rows, options) {
212
- this.snapshot = await this.writeTables(this.id, { tableName, rows, mode: 'replaceTable' }, options);
243
+ async replaceTable(tableName, data, options) {
244
+ if (data instanceof Blob) {
245
+ if (!options || !('format' in options)) {
246
+ throw new PermuteApiError({
247
+ status: 0,
248
+ code: 'invalid_upload_format',
249
+ message: 'CSV file uploads require format: csv',
250
+ });
251
+ }
252
+ const { format, compression, rowCount, ...requestOptions } = options;
253
+ this.snapshot = await this.writeTables(this.id, { tableName, file: data, mode: 'replaceTable', format, compression, rowCount }, requestOptions);
254
+ return this;
255
+ }
256
+ this.snapshot = await this.writeTables(this.id, { tableName, rows: data, mode: 'replaceTable' }, options);
213
257
  return this;
214
258
  }
215
259
  async appendTable(tableName, rows, options) {
@@ -251,6 +295,9 @@ export class PermuteFileDataset {
251
295
  }
252
296
  }
253
297
  export class PermuteClient {
298
+ organizations;
299
+ organizationMembers;
300
+ workspaceMembers;
254
301
  permissions;
255
302
  accessRequests;
256
303
  revisions;
@@ -258,6 +305,7 @@ export class PermuteClient {
258
305
  skills;
259
306
  organizationSkills;
260
307
  actions;
308
+ actionTriggers;
261
309
  emails;
262
310
  databases;
263
311
  apps;
@@ -305,6 +353,24 @@ export class PermuteClient {
305
353
  delete: this.bindMethod('permissionsDelete', (request) => (permissionId, options) => request({ pathParams: { permissionId } }, options)),
306
354
  inspect: this.bindMethod('permissionsInspect', (request) => (body, options) => request({ body }, options)),
307
355
  };
356
+ this.organizations = {
357
+ list: this.bindMethod('organizationsList', (apiRequest) => (options) => apiRequest({}, options)),
358
+ get: this.bindMethod('organizationsGet', (apiRequest) => (organizationId, options) => apiRequest({ pathParams: { organizationId } }, options)),
359
+ };
360
+ this.organizationMembers = {
361
+ list: this.bindMethod('organizationMembersList', (apiRequest) => (organizationId, options) => apiRequest({ pathParams: { organizationId } }, options)),
362
+ invite: this.bindMethod('organizationMembersInvite', (request) => (organizationId, body, options) => request({ pathParams: { organizationId }, body }, options)),
363
+ resendInvitation: this.bindMethod('organizationMembersResendInvitation', (request) => (organizationId, userId, options) => request({ pathParams: { organizationId, userId } }, options)),
364
+ updateRole: this.bindMethod('organizationMembersUpdateRole', (request) => (organizationId, userId, body, options) => request({ pathParams: { organizationId, userId }, body }, options)),
365
+ remove: this.bindMethod('organizationMembersRemove', (request) => (organizationId, userId, options) => request({ pathParams: { organizationId, userId } }, options)),
366
+ };
367
+ this.workspaceMembers = {
368
+ list: this.bindMethod('workspaceMembersList', (apiRequest) => (workspaceId, options) => apiRequest({ pathParams: { workspaceId } }, options)),
369
+ invite: this.bindMethod('workspaceMembersInvite', (request) => (workspaceId, body, options) => request({ pathParams: { workspaceId }, body }, options)),
370
+ resendInvitation: this.bindMethod('workspaceMembersResendInvitation', (request) => (workspaceId, userId, options) => request({ pathParams: { workspaceId, userId } }, options)),
371
+ updateRole: this.bindMethod('workspaceMembersUpdateRole', (request) => (workspaceId, userId, body, options) => request({ pathParams: { workspaceId, userId }, body }, options)),
372
+ remove: this.bindMethod('workspaceMembersRemove', (request) => (workspaceId, userId, options) => request({ pathParams: { workspaceId, userId } }, options)),
373
+ };
308
374
  this.accessRequests = {
309
375
  create: this.bindMethod('accessRequestsCreate', (request) => (body, options) => request({ body }, options)),
310
376
  list: this.bindMethod('accessRequestsList', (request) => (query, options) => request({ query }, options)),
@@ -344,6 +410,14 @@ export class PermuteClient {
344
410
  saveDefinition: this.bindMethod('actionDefinitionSave', (apiRequest) => (actionId, body, options) => apiRequest({ pathParams: { actionId }, body }, options)),
345
411
  invoke: this.bindMethod('actionsInvoke', (apiRequest) => (actionId, body, options) => apiRequest({ pathParams: { actionId }, body }, options)),
346
412
  };
413
+ this.actionTriggers = {
414
+ list: this.bindMethod('actionTriggersList', (apiRequest) => (actionId, options) => apiRequest({ pathParams: { actionId } }, options)),
415
+ create: this.bindMethod('actionTriggersCreate', (apiRequest) => (actionId, body, options) => apiRequest({ pathParams: { actionId }, body }, options)),
416
+ get: this.bindMethod('actionTriggersGet', (apiRequest) => (actionId, triggerId, options) => apiRequest({ pathParams: { actionId, triggerId } }, options)),
417
+ update: this.bindMethod('actionTriggersUpdate', (apiRequest) => (actionId, triggerId, body, options) => apiRequest({ pathParams: { actionId, triggerId }, body }, options)),
418
+ delete: this.bindMethod('actionTriggersDelete', (apiRequest) => (actionId, triggerId, options) => apiRequest({ pathParams: { actionId, triggerId } }, options)),
419
+ runs: this.bindMethod('actionTriggerRunsList', (apiRequest) => (actionId, triggerId, options) => apiRequest({ pathParams: { actionId, triggerId } }, options)),
420
+ };
347
421
  this.emails = {
348
422
  get: this.bindMethod('emailsGet', (request) => (options) => request({}, options)),
349
423
  send: this.bindMethod('emailsSend', (request) => (body, options) => request({ body }, options)),
@@ -475,30 +549,39 @@ export class PermuteClient {
475
549
  };
476
550
  const readDataset = this.bindMethod('datasetsGet', (apiRequest) => (datasetId, requestOptions) => apiRequest({ pathParams: { datasetId } }, requestOptions));
477
551
  const writeDatasetTables = async (datasetId, request, requestOptions) => {
478
- const encoded = encodeDatasetWrite(request);
479
- const body = request.mode === 'replace'
552
+ const encoded = 'file' in request
553
+ ? { data: request.file, contentHash: await hashDatasetFile(request.file, requestOptions?.signal) }
554
+ : encodeDatasetWrite(request);
555
+ const uploadMetadata = { sizeBytes: encoded.data.size, contentHash: encoded.contentHash };
556
+ const body = 'file' in request
480
557
  ? {
481
- mode: request.mode,
482
- tables: encoded.tables,
483
- sizeBytes: encoded.data.size,
484
- contentHash: encoded.contentHash,
558
+ mode: 'replaceTable',
559
+ tableName: request.tableName,
560
+ format: request.format,
561
+ compression: request.compression,
562
+ rowCount: request.rowCount,
563
+ ...uploadMetadata,
485
564
  }
486
- : request.mode === 'replaceTable'
565
+ : request.mode === 'replace'
487
566
  ? {
488
567
  mode: request.mode,
489
- tableName: request.tableName,
490
- rowCount: request.rows.length,
491
- sizeBytes: encoded.data.size,
492
- contentHash: encoded.contentHash,
568
+ tables: Object.entries(request.tables).map(([name, rows]) => ({ name, rowCount: rows.length })),
569
+ ...uploadMetadata,
493
570
  }
494
- : {
495
- mode: request.mode,
496
- tableName: request.tableName,
497
- rowCount: request.rows.length,
498
- sizeBytes: encoded.data.size,
499
- contentHash: encoded.contentHash,
500
- batchId: request.batchId,
501
- };
571
+ : request.mode === 'replaceTable'
572
+ ? {
573
+ mode: request.mode,
574
+ tableName: request.tableName,
575
+ rowCount: request.rows.length,
576
+ ...uploadMetadata,
577
+ }
578
+ : {
579
+ mode: request.mode,
580
+ tableName: request.tableName,
581
+ rowCount: request.rows.length,
582
+ ...uploadMetadata,
583
+ batchId: request.batchId,
584
+ };
502
585
  const initialized = await this.request('datasetsWriteTables', { pathParams: { datasetId }, body }, requestOptions);
503
586
  if (initialized.write.status === 'completed')
504
587
  return initialized.dataset;
@@ -662,7 +745,7 @@ export class PermuteClient {
662
745
  ...options?.headers,
663
746
  };
664
747
  const workspaceId = options?.workspaceId ?? this.workspaceId;
665
- if (route.scope === 'workspace' && workspaceId) {
748
+ if (route.scope === 'workspace' && workspaceId && !input.pathParams?.workspaceId) {
666
749
  headers['X-Workspace-Id'] = workspaceId;
667
750
  }
668
751
  if (body !== undefined) {