@pipelex/sdk 0.50.0 → 0.52.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 +13 -0
- package/dist/cli/script.d.ts +1 -1
- package/dist/client.d.ts +106 -2
- package/dist/client.js +138 -10
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +6 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/product-models.d.ts +144 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +1 -0
- package/docs/errors.md +6 -2
- package/docs/method-copy.md +128 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -167,6 +167,18 @@ for await (const saved of client.iterateMethodExamples(created.method_id)) {
|
|
|
167
167
|
// every example, oldest first
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
// Copy a method into a team, or into your personal workspace, with its draft, its form inputs and
|
|
171
|
+
// its examples (web sessions only). A bundle naming storage the destination cannot read, or, in a
|
|
172
|
+
// duplicate, a file under the original's run files, which erasing the original would remove,
|
|
173
|
+
// answers `refused`, a verdict rather than an error; the key makes a retry copy once.
|
|
174
|
+
const copy = await client.copyMethod(
|
|
175
|
+
created.method_id,
|
|
176
|
+
{ destination_org_id: "org_team", expected_draft_updated_at: draft.updated_at },
|
|
177
|
+
{ idempotencyKey: crypto.randomUUID() },
|
|
178
|
+
);
|
|
179
|
+
if (copy.outcome === "copied") console.log(copy.method.copied_from); // the original it names
|
|
180
|
+
const copies = await client.listMethodCopies(created.method_id); // where the copies went
|
|
181
|
+
|
|
170
182
|
// The organization's files, newest first: uploads, test files and the files runs made. Each row's
|
|
171
183
|
// `uri` goes into a run's inputs as any address does. A filtered page can be empty and still carry
|
|
172
184
|
// a nextCursor; iterateFiles follows it.
|
|
@@ -273,6 +285,7 @@ These pages ship inside the published package, so a reader who has only installe
|
|
|
273
285
|
| [`docs/method-trees.md`](./docs/method-trees.md) | A method's tree: the four forms a run, a validation, a save and the crate routes take it in (a directory on disk among them), how it travels inline or by hash and what is uploaded, the files a step reads, the template check's warnings and which step reads which file of the tree, the deployment's limits, the read's listing with its links, `writeMethodTree`, and `MethodFileTransferError` |
|
|
274
286
|
| [`docs/files.md`](./docs/files.md) | An organization's files: `listFiles` and `iterateFiles`, the row, the whole record and its provenance, the query and its repeated filters, paging through filtered pages, one file and its uses, the records of an address, the rename rule, links that name the file, a zip of files and its caps, a run's files, the copy into a method's draft, and an upload recorded as a method's test file with the refusals that come with it |
|
|
275
287
|
| [`docs/method-examples.md`](./docs/method-examples.md) | A method's saved examples: the create, the paged list and its iterator, the edit that raises the input revision, the latest-run write and its `recorded` or `stale` verdict, the build assistant's save and its `kept` or `created` verdict, how an integer past `Number.MAX_SAFE_INTEGER` in the inputs stays exact, and what is refused before sending |
|
|
288
|
+
| [`docs/method-copy.md`](./docs/method-copy.md) | Copying a method into another organization or duplicating it: what a copy holds and the stored files it copies, the `copied` or `refused` verdict, every refusal, the copy's idempotency key, the copies a method lists, and `copied_from` |
|
|
276
289
|
| [`docs/organization-branding.md`](./docs/organization-branding.md) | An organization's logos and colours: `getBranding`, which every member and API key reads, `updateOrganizationBranding`, an admin's write that sets, clears or keeps each key, and how a refused logo or colour names its slot in `errors` so a settings screen shows the sentence under it |
|
|
277
290
|
| [`docs/exact-integers.md`](./docs/exact-integers.md) | Which reads keep an integer past `Number.MAX_SAFE_INTEGER` exact, as a `bigint` (a run's inputs and outputs, an example's inputs), how a request body writes it back with its digits, and `parseExactJson` and `stringifyExactJson` for code showing such data as JSON text |
|
|
278
291
|
| [`docs/crate-routes.md`](./docs/crate-routes.md) | `resolve`, `codegen` and `pipeIo` — the method's tree as the protocol's `files` map, with its dependencies, the normalized crate, the stamped types, and a method's I/O artifacts without a validation — and the offline `runCodegenCheck` that guards a committed tree |
|
package/dist/cli/script.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { CommandIO } from "./io.js";
|
|
|
15
15
|
* update notice and npm 12's run notices off the script's stderr, which carries the command's
|
|
16
16
|
* own lines, while npm's warnings and errors still print.
|
|
17
17
|
*/
|
|
18
|
-
export declare const SCRIPT_RUNNER = "npx --yes --no-update-notifier --loglevel=warn @pipelex/sdk@0.
|
|
18
|
+
export declare const SCRIPT_RUNNER = "npx --yes --no-update-notifier --loglevel=warn @pipelex/sdk@0.52.0";
|
|
19
19
|
export declare const SCRIPT_WRITER = "@pipelex/sdk";
|
|
20
20
|
export declare const SCRIPT_NEEDS = "Node 22.12 or later";
|
|
21
21
|
/** Run `pipelex-sdk script` and return its exit code. */
|
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MTHDSProtocol, ModelCategory, ModelDeck, RunOptions, StartOptions, VersionInfo } from "mthds/protocol";
|
|
2
2
|
import type { CodegenRequest, CodegenResponse, CrateRouteOptions, DictPipeOutput, FormatResponse, LintResponse, ModelCheckCategory, ModelReferenceVerdict, PipeIORequest, PipeIOResponse, PipelexRunResultStart, PipelexValidateSource, PipelexValidationResult, ResolveRequest, ResolveResponse, ValidateMethodSelector } from "./models.js";
|
|
3
3
|
import { type GetRunResultOptions, type RunProgress, type RunRead, type RunResults, type RunResultState, type StartAndWaitForResultOptions, type WaitForResultOptions } from "./runs.js";
|
|
4
|
-
import type { BillingPortalResponse, ChangePlanResponse, CheckoutResponse, DraftFileFromFileInput, DraftFileFromFileResult, FileArchive, FileArchiveRequest, FileDetail, FileLinkTarget, FileLinksResult, FilePage, FileRecord, FileRecordList, FileRenameInput, FileSummary, GetFileQuery, GetFilesByUriOptions, InvoiceView, Membership, MembershipsResponse, OrganizationBranding, OrganizationBrandingUpdate, ListFilesQuery, ListMethodExamplesQuery, ListMethodsQuery, ListMethodVersionsQuery, ListRunFilesQuery, MethodData, MethodDeletionAccepted, MethodDetail, MethodDraftInput, MethodExample, MethodExampleAssistantSaveInput, MethodExampleAssistantSaveResult, MethodExampleCreateInput, MethodExampleLatestRunInput, MethodExampleLatestRunResult, MethodExamplePage, MethodExampleUpdateInput, MethodPage, MethodPublishInput, MethodPublishResult, MethodRenameInput, MethodSummary, MethodVersion, MethodVersionPage, MethodWriteInput, OnboardingSubmission, PipelexApiKeyCreated, PipelexApiKeyList, ListRunsQuery, RunHistoryItem, RunDetail, RunFilesPage, RunPage, PlanView, ResolvedStorageUrl, SavedMethodFile, SubscriptionResponse, UpdateRunInput, UploadGrant, UploadGrantInput, UploadInput, UploadedFile, UserProfile } from "./product-models.js";
|
|
4
|
+
import type { BillingPortalResponse, ChangePlanResponse, CheckoutResponse, DraftFileFromFileInput, DraftFileFromFileResult, FileArchive, FileArchiveRequest, FileDetail, FileLinkTarget, FileLinksResult, FilePage, FileRecord, FileRecordList, FileRenameInput, FileSummary, GetFileQuery, GetFilesByUriOptions, InvoiceView, Membership, MembershipsResponse, OrganizationBranding, OrganizationBrandingUpdate, ListFilesQuery, ListMethodCopiesQuery, ListMethodExamplesQuery, ListMethodsQuery, ListMethodVersionsQuery, ListRunFilesQuery, MethodCopyInput, MethodCopyPage, MethodCopyResult, MethodData, MethodDeletionAccepted, MethodDetail, MethodDraftInput, MethodExample, MethodExampleAssistantSaveInput, MethodExampleAssistantSaveResult, MethodExampleCreateInput, MethodExampleLatestRunInput, MethodExampleLatestRunResult, MethodExamplePage, MethodExampleUpdateInput, MethodPage, MethodPublishInput, MethodPublishResult, MethodRenameInput, MethodSummary, MethodVersion, MethodVersionPage, MethodWriteInput, OnboardingSubmission, PipelexApiKeyCreated, PipelexApiKeyList, ListRunsQuery, RunHistoryItem, RunDetail, RunFilesPage, RunPage, PlanView, ResolvedStorageUrl, SavedMethodFile, SubscriptionResponse, UpdateRunInput, UploadGrant, UploadGrantInput, UploadInput, UploadedFile, UserProfile } from "./product-models.js";
|
|
5
5
|
import type { MethodTreeForms, MethodTreeLimits } from "./method-trees.js";
|
|
6
6
|
import type { AppInfo } from "./user-agent.js";
|
|
7
7
|
import type { UploadableAsset, UploadFileOptions, UploadRecord } from "./upload.js";
|
|
@@ -122,6 +122,36 @@ export interface PipelexStartRequestOptions {
|
|
|
122
122
|
*/
|
|
123
123
|
idempotencyKey?: string | null;
|
|
124
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* What a method copy sends beside its body: the request's own header, which no copy argument
|
|
127
|
+
* reaches, camelCase since it is no wire field of the copy.
|
|
128
|
+
*/
|
|
129
|
+
export interface MethodCopyRequestOptions {
|
|
130
|
+
/**
|
|
131
|
+
* The copy's idempotency key, sent as the `Idempotency-Key` header of
|
|
132
|
+
* `POST /v1/methods/{id}/copy`, and not sent at all when absent or `null`. Generate one per copy
|
|
133
|
+
* the caller means to make (a `crypto.randomUUID()` is a good key), and keep it, with the
|
|
134
|
+
* arguments unchanged, across that copy's retries.
|
|
135
|
+
*
|
|
136
|
+
* The hosted API stores the copy's answer under the key for 24 hours, scoped to the caller and
|
|
137
|
+
* the method's organization, so the same copy sent again under the same key is answered with the
|
|
138
|
+
* stored answer and copies nothing more: a copy whose answer was lost, as on a client timeout,
|
|
139
|
+
* can be sent again unchanged and makes one copy, not two. A verdict and a `4xx` refusal of the
|
|
140
|
+
* copy are stored alike, so the same copy under the same key is answered the same again; a `5xx`
|
|
141
|
+
* and a refusal for its timing (a `408`, `425` or `429`) are not stored, and a copy that failed
|
|
142
|
+
* with one left nothing visible, but for the `503` whose `detail` names the copy (see
|
|
143
|
+
* `copyMethod`), so the same key sends it afresh. The same key with a different body is refused
|
|
144
|
+
* (`409` `conflict`, `input` and not retryable, which its `errorDomain` tells from the `runtime`
|
|
145
|
+
* `conflict` of an incomplete tree), as is a key whose earlier request is still being handled
|
|
146
|
+
* (`409` `idempotency_key_in_use`, retryable: send it again under the same key in a few
|
|
147
|
+
* seconds); a changed copy, such as one with a fresh draft token, goes under a new key. See
|
|
148
|
+
* `docs/method-copy.md`.
|
|
149
|
+
*
|
|
150
|
+
* A key is held to the rule a start's is (`PipelexStartRequestOptions.idempotencyKey`): 1 to
|
|
151
|
+
* 255 visible ASCII characters, anything else a `RequestArgumentError` before any request.
|
|
152
|
+
*/
|
|
153
|
+
idempotencyKey?: string | null;
|
|
154
|
+
}
|
|
125
155
|
/**
|
|
126
156
|
* `start()` / `startAndWaitForResult()` options — the same, for the durable path, plus the start
|
|
127
157
|
* request's own `idempotencyKey`.
|
|
@@ -231,7 +261,7 @@ export declare class PipelexApiClient implements HostedProtocolSurface {
|
|
|
231
261
|
* resolved absolute URL. `mayStartRun` marks a request that may create a run,
|
|
232
262
|
* whose `ApiUnreachableError` then says whether it may have (see `RunCreation`).
|
|
233
263
|
* `idempotencyKey`, already checked by `idempotencyKeyOf`, is sent as the
|
|
234
|
-
* `Idempotency-Key` header; only `start`
|
|
264
|
+
* `Idempotency-Key` header; only `start` and `copyMethod` pass one.
|
|
235
265
|
*/
|
|
236
266
|
private requestRaw;
|
|
237
267
|
/**
|
|
@@ -1215,6 +1245,80 @@ export declare class PipelexApiClient implements HostedProtocolSurface {
|
|
|
1215
1245
|
* never rewritten as one, which names no tree to serve.
|
|
1216
1246
|
*/
|
|
1217
1247
|
getMethodVersion(methodId: string, version: number): Promise<MethodVersion>;
|
|
1248
|
+
/**
|
|
1249
|
+
* Copy a method into an organization the caller may change methods in —
|
|
1250
|
+
* `POST /v1/methods/{id}/copy`: the method's own organization for a duplicate, another team, or
|
|
1251
|
+
* the caller's personal workspace.
|
|
1252
|
+
*
|
|
1253
|
+
* The copy is a new, unpublished method of the destination, the caller's, holding the
|
|
1254
|
+
* original's draft tree (the same `draft_digest`), its name unless `name` gives another, its
|
|
1255
|
+
* form inputs and every saved example, each stored file they name copied into the destination,
|
|
1256
|
+
* and `copied_from` naming the original. Published versions and runs are not copied, and the
|
|
1257
|
+
* original stays as it was. The copy takes the draft, never the latest published version; with
|
|
1258
|
+
* `expected_draft_updated_at`, it takes exactly the draft the caller last saw.
|
|
1259
|
+
*
|
|
1260
|
+
* The answer is a `MethodCopyResult` discriminated on `outcome` — branch on it: `copied` with
|
|
1261
|
+
* the copy as `method`, `examples_copied` and `stored_files_copied`; `refused` with the
|
|
1262
|
+
* `reason` `bundle_names_other_storage`, a `message` and every `references` entry (`{ path, uri
|
|
1263
|
+
* }`) a `.mthds` file of the draft holds that the destination cannot read and, in a duplicate,
|
|
1264
|
+
* that names a file under the original's run files, which erasing the original would remove,
|
|
1265
|
+
* nothing written.
|
|
1266
|
+
*
|
|
1267
|
+
* The route serves web sessions only: a Pipelex API key belongs to one organization and cannot
|
|
1268
|
+
* speak for another. `options.idempotencyKey` is sent as the `Idempotency-Key` header, so a copy
|
|
1269
|
+
* sent again under the same key is answered with the first one's answer rather than copied
|
|
1270
|
+
* twice (see `MethodCopyRequestOptions`).
|
|
1271
|
+
*
|
|
1272
|
+
* Takes a bare catalog id, as `getMethod` does. Sends `destination_org_id` and, when given,
|
|
1273
|
+
* `name` and `expected_draft_updated_at`, and nothing else: the body is closed, and a `null`
|
|
1274
|
+
* reads as not given. Throws `RequestArgumentError` before any request for a
|
|
1275
|
+
* `destination_org_id` that is not a non-empty string, a `name` or an
|
|
1276
|
+
* `expected_draft_updated_at` given that is not a string, and a malformed idempotency key.
|
|
1277
|
+
* Throws `ApiResponseError` when no verdict was produced, each refusal writing nothing a reader
|
|
1278
|
+
* sees but for the `503` whose `detail` names the copy: `403` `forbidden` for a request that is
|
|
1279
|
+
* not from a web session; `403`
|
|
1280
|
+
* `role_not_permitted` for a role that may not change methods, in the method's organization or
|
|
1281
|
+
* in the destination; `404` `organization_not_found` for a destination the caller does not
|
|
1282
|
+
* belong to, still being set up or unknown, alike; `404` `not_found` for an unknown method;
|
|
1283
|
+
* `409` `method_being_deleted`; `409` `method_update_conflict` for a draft that moved since
|
|
1284
|
+
* `expected_draft_updated_at`; `409` `conflict`, `runtime` and not retryable, for a draft whose
|
|
1285
|
+
* tree the method's organization holds no record of or no longer holds every file of; `413`
|
|
1286
|
+
* `payload_too_large` for a copied example, or the copy's row, too large for one stored item,
|
|
1287
|
+
* the row keeping room for the summary a publish adds, so a `name` that would leave none is
|
|
1288
|
+
* refused; `422` `validation_failed` for a body that does not validate, such as a name holding a
|
|
1289
|
+
* lone surrogate; `422` `method_copy_too_large` past one of the copy's bounds on the stored files
|
|
1290
|
+
* it copies, named by `problemDocument.limit` (`MethodCopyLimit`); a retryable `502` when the
|
|
1291
|
+
* platform could not read the caller's memberships; a `500` `storage_backend_failed`, retryable
|
|
1292
|
+
* unless the platform has no usable credentials for storage; and a retryable `503` past the
|
|
1293
|
+
* copy's deadline, when a stored file it copies is written over meanwhile, or when DynamoDB
|
|
1294
|
+
* fails. When DynamoDB failed both the copy's row write and the read that would confirm it, that
|
|
1295
|
+
* `503`'s `detail` names the copy's method id: the copy may stand in the destination, so look for
|
|
1296
|
+
* that id there before copying again, since a retry may make a second copy. An answer whose
|
|
1297
|
+
* `outcome` is neither arm's, a `copied` one whose `method` is not an object or a `refused` one
|
|
1298
|
+
* whose `references` is not an array, is an answer the SDK cannot read, thrown as an
|
|
1299
|
+
* `ApiResponseError` too.
|
|
1300
|
+
*/
|
|
1301
|
+
copyMethod(methodId: string, input: MethodCopyInput, options?: MethodCopyRequestOptions): Promise<MethodCopyResult>;
|
|
1302
|
+
/**
|
|
1303
|
+
* One page of the copies made of a method, in the order of the copies' ids, which says nothing
|
|
1304
|
+
* of when each was made — `GET /v1/methods/{id}/copies`.
|
|
1305
|
+
*
|
|
1306
|
+
* Each row is the `MethodCopyMarker` the original keeps of a copy: the copy's id, the
|
|
1307
|
+
* destination's id, the member who made it and when. The destination is named by its id alone,
|
|
1308
|
+
* never its name, so a surface shows a destination's name only from the reader's own
|
|
1309
|
+
* memberships, and a copy in an organization the reader does not belong to stays a bare id. A
|
|
1310
|
+
* marker stays when its copy is erased, and the copy then reads as not found. Pass the page's
|
|
1311
|
+
* `nextCursor` back as `cursor` to continue; `null` is the last page. A method never copied,
|
|
1312
|
+
* and a copy itself, answer an empty page. The listing is open to every member of the method's
|
|
1313
|
+
* organization, a runner included, as every read of a method is.
|
|
1314
|
+
*
|
|
1315
|
+
* Takes a bare catalog id, as `getMethod` does. Throws `ApiResponseError`: `404` `not_found` for
|
|
1316
|
+
* an unknown method, `409` `method_being_deleted`, `400` `malformed_request` for a cursor the
|
|
1317
|
+
* listing did not issue for this method, `422` for a `limit` outside 1 to 100. A page whose
|
|
1318
|
+
* `items` is not an array or whose `next_cursor` is neither a string nor `null` is an answer the
|
|
1319
|
+
* SDK cannot read, thrown as an `ApiResponseError` too.
|
|
1320
|
+
*/
|
|
1321
|
+
listMethodCopies(methodId: string, query?: ListMethodCopiesQuery): Promise<MethodCopyPage>;
|
|
1218
1322
|
/**
|
|
1219
1323
|
* Erase a method and everything it produced — `DELETE /v1/methods/{id}`.
|
|
1220
1324
|
*
|
package/dist/client.js
CHANGED
|
@@ -233,6 +233,11 @@ const ARCHIVE_RUN_ID_ARGUMENT = "a run_id naming the run whose files to zip, a n
|
|
|
233
233
|
const ARCHIVE_NAME_ARGUMENT = "a name for the zip, a string, or none";
|
|
234
234
|
const DRAFT_PATH_ARGUMENT = "the tree path the file takes in the draft, a non-empty string";
|
|
235
235
|
const DRAFT_TOKEN_ARGUMENT = "the draft token the caller last read, the method's `updated_at`, a string, or none";
|
|
236
|
+
/** What each argument of a method copy is, as a refusal of it names it. */
|
|
237
|
+
const COPY_DESTINATION_ARGUMENT = "a destination_org_id, the canonical id of the organization to copy into, a non-empty string";
|
|
238
|
+
const COPY_NAME_ARGUMENT = "a name for the copy, a string, or none";
|
|
239
|
+
const COPY_DRAFT_TOKEN_ARGUMENT = "an expected_draft_updated_at, the draft token the caller last read, the method's " +
|
|
240
|
+
"`updated_at`, a string, or none";
|
|
236
241
|
/**
|
|
237
242
|
* The refusal of an argument a call cannot take, worded as every such refusal is:
|
|
238
243
|
* `<call>() takes <what>; got <got>.`, with what made it unusable as `cause` when there is one.
|
|
@@ -341,6 +346,8 @@ const isString = (value) => typeof value === "string";
|
|
|
341
346
|
const FILE_ARCHIVE_STATES = ["queued", "building", "ready", "failed"];
|
|
342
347
|
/** What a copy into a draft did in the blob store, the field its answer is discriminated on. */
|
|
343
348
|
const DRAFT_FILE_BLOBS = ["copied", "already_held"];
|
|
349
|
+
/** What a method copy did, the field its answer is discriminated on. */
|
|
350
|
+
const METHOD_COPY_OUTCOMES = ["copied", "refused"];
|
|
344
351
|
/** Whether a run's records are all written, the field a reader of its files branches on. */
|
|
345
352
|
const RUN_FILES_STATES = ["listing", "listed"];
|
|
346
353
|
/** The lists of a page of a run's files, each an array on every page. */
|
|
@@ -479,7 +486,7 @@ export class PipelexApiClient {
|
|
|
479
486
|
* resolved absolute URL. `mayStartRun` marks a request that may create a run,
|
|
480
487
|
* whose `ApiUnreachableError` then says whether it may have (see `RunCreation`).
|
|
481
488
|
* `idempotencyKey`, already checked by `idempotencyKeyOf`, is sent as the
|
|
482
|
-
* `Idempotency-Key` header; only `start`
|
|
489
|
+
* `Idempotency-Key` header; only `start` and `copyMethod` pass one.
|
|
483
490
|
*/
|
|
484
491
|
async requestRaw(method, url, options = {}) {
|
|
485
492
|
const hasBody = options.body !== undefined;
|
|
@@ -692,6 +699,7 @@ export class PipelexApiClient {
|
|
|
692
699
|
body,
|
|
693
700
|
timeoutMs: options.timeoutMs ?? POLL_REQUEST_TIMEOUT_MS,
|
|
694
701
|
signal: options.signal,
|
|
702
|
+
idempotencyKey: options.idempotencyKey,
|
|
695
703
|
});
|
|
696
704
|
if (res.status < 200 || res.status >= 300) {
|
|
697
705
|
this.throwApiResponseError(method, endpoint, res);
|
|
@@ -2347,6 +2355,125 @@ export class PipelexApiClient {
|
|
|
2347
2355
|
const res = await this.requestProductAnswer("GET", endpoint, undefined);
|
|
2348
2356
|
return this.readSavedTree("GET", endpoint, withLinks(res), "a published version");
|
|
2349
2357
|
}
|
|
2358
|
+
/**
|
|
2359
|
+
* Copy a method into an organization the caller may change methods in —
|
|
2360
|
+
* `POST /v1/methods/{id}/copy`: the method's own organization for a duplicate, another team, or
|
|
2361
|
+
* the caller's personal workspace.
|
|
2362
|
+
*
|
|
2363
|
+
* The copy is a new, unpublished method of the destination, the caller's, holding the
|
|
2364
|
+
* original's draft tree (the same `draft_digest`), its name unless `name` gives another, its
|
|
2365
|
+
* form inputs and every saved example, each stored file they name copied into the destination,
|
|
2366
|
+
* and `copied_from` naming the original. Published versions and runs are not copied, and the
|
|
2367
|
+
* original stays as it was. The copy takes the draft, never the latest published version; with
|
|
2368
|
+
* `expected_draft_updated_at`, it takes exactly the draft the caller last saw.
|
|
2369
|
+
*
|
|
2370
|
+
* The answer is a `MethodCopyResult` discriminated on `outcome` — branch on it: `copied` with
|
|
2371
|
+
* the copy as `method`, `examples_copied` and `stored_files_copied`; `refused` with the
|
|
2372
|
+
* `reason` `bundle_names_other_storage`, a `message` and every `references` entry (`{ path, uri
|
|
2373
|
+
* }`) a `.mthds` file of the draft holds that the destination cannot read and, in a duplicate,
|
|
2374
|
+
* that names a file under the original's run files, which erasing the original would remove,
|
|
2375
|
+
* nothing written.
|
|
2376
|
+
*
|
|
2377
|
+
* The route serves web sessions only: a Pipelex API key belongs to one organization and cannot
|
|
2378
|
+
* speak for another. `options.idempotencyKey` is sent as the `Idempotency-Key` header, so a copy
|
|
2379
|
+
* sent again under the same key is answered with the first one's answer rather than copied
|
|
2380
|
+
* twice (see `MethodCopyRequestOptions`).
|
|
2381
|
+
*
|
|
2382
|
+
* Takes a bare catalog id, as `getMethod` does. Sends `destination_org_id` and, when given,
|
|
2383
|
+
* `name` and `expected_draft_updated_at`, and nothing else: the body is closed, and a `null`
|
|
2384
|
+
* reads as not given. Throws `RequestArgumentError` before any request for a
|
|
2385
|
+
* `destination_org_id` that is not a non-empty string, a `name` or an
|
|
2386
|
+
* `expected_draft_updated_at` given that is not a string, and a malformed idempotency key.
|
|
2387
|
+
* Throws `ApiResponseError` when no verdict was produced, each refusal writing nothing a reader
|
|
2388
|
+
* sees but for the `503` whose `detail` names the copy: `403` `forbidden` for a request that is
|
|
2389
|
+
* not from a web session; `403`
|
|
2390
|
+
* `role_not_permitted` for a role that may not change methods, in the method's organization or
|
|
2391
|
+
* in the destination; `404` `organization_not_found` for a destination the caller does not
|
|
2392
|
+
* belong to, still being set up or unknown, alike; `404` `not_found` for an unknown method;
|
|
2393
|
+
* `409` `method_being_deleted`; `409` `method_update_conflict` for a draft that moved since
|
|
2394
|
+
* `expected_draft_updated_at`; `409` `conflict`, `runtime` and not retryable, for a draft whose
|
|
2395
|
+
* tree the method's organization holds no record of or no longer holds every file of; `413`
|
|
2396
|
+
* `payload_too_large` for a copied example, or the copy's row, too large for one stored item,
|
|
2397
|
+
* the row keeping room for the summary a publish adds, so a `name` that would leave none is
|
|
2398
|
+
* refused; `422` `validation_failed` for a body that does not validate, such as a name holding a
|
|
2399
|
+
* lone surrogate; `422` `method_copy_too_large` past one of the copy's bounds on the stored files
|
|
2400
|
+
* it copies, named by `problemDocument.limit` (`MethodCopyLimit`); a retryable `502` when the
|
|
2401
|
+
* platform could not read the caller's memberships; a `500` `storage_backend_failed`, retryable
|
|
2402
|
+
* unless the platform has no usable credentials for storage; and a retryable `503` past the
|
|
2403
|
+
* copy's deadline, when a stored file it copies is written over meanwhile, or when DynamoDB
|
|
2404
|
+
* fails. When DynamoDB failed both the copy's row write and the read that would confirm it, that
|
|
2405
|
+
* `503`'s `detail` names the copy's method id: the copy may stand in the destination, so look for
|
|
2406
|
+
* that id there before copying again, since a retry may make a second copy. An answer whose
|
|
2407
|
+
* `outcome` is neither arm's, a `copied` one whose `method` is not an object or a `refused` one
|
|
2408
|
+
* whose `references` is not an array, is an answer the SDK cannot read, thrown as an
|
|
2409
|
+
* `ApiResponseError` too.
|
|
2410
|
+
*/
|
|
2411
|
+
async copyMethod(methodId, input, options = {}) {
|
|
2412
|
+
const call = "copyMethod";
|
|
2413
|
+
const endpoint = `${methodPath(methodId, call)}/copy`;
|
|
2414
|
+
const { destination_org_id: destinationOrgId, name, expected_draft_updated_at: token, } = input ?? {};
|
|
2415
|
+
const body = {
|
|
2416
|
+
destination_org_id: requireNonEmptyId(destinationOrgId, call, COPY_DESTINATION_ARGUMENT),
|
|
2417
|
+
};
|
|
2418
|
+
if (name != null) {
|
|
2419
|
+
if (typeof name !== "string") {
|
|
2420
|
+
throw argumentRefusal(call, COPY_NAME_ARGUMENT, describeNumberArgument(name));
|
|
2421
|
+
}
|
|
2422
|
+
body.name = name;
|
|
2423
|
+
}
|
|
2424
|
+
if (token != null) {
|
|
2425
|
+
if (typeof token !== "string") {
|
|
2426
|
+
throw argumentRefusal(call, COPY_DRAFT_TOKEN_ARGUMENT, describeNumberArgument(token));
|
|
2427
|
+
}
|
|
2428
|
+
body.expected_draft_updated_at = token;
|
|
2429
|
+
}
|
|
2430
|
+
const idempotencyKey = idempotencyKeyOf(options, "copy the method");
|
|
2431
|
+
const res = await this.requestProductAnswer("POST", endpoint, body, { idempotencyKey });
|
|
2432
|
+
const answer = this.readObjectAnswer("POST", endpoint, res);
|
|
2433
|
+
let fault;
|
|
2434
|
+
switch (answer.outcome) {
|
|
2435
|
+
case "copied":
|
|
2436
|
+
if (!isPlainObject(answer.method)) {
|
|
2437
|
+
fault = "a `copied` copy result whose `method` is not an object";
|
|
2438
|
+
}
|
|
2439
|
+
break;
|
|
2440
|
+
case "refused":
|
|
2441
|
+
if (!Array.isArray(answer.references)) {
|
|
2442
|
+
fault = "a `refused` copy result whose `references` is not an array";
|
|
2443
|
+
}
|
|
2444
|
+
break;
|
|
2445
|
+
default:
|
|
2446
|
+
fault = `a copy result whose \`outcome\` is not ${inWords(METHOD_COPY_OUTCOMES.map((outcome) => `\`${outcome}\``), "or")}`;
|
|
2447
|
+
}
|
|
2448
|
+
if (fault !== undefined) {
|
|
2449
|
+
throw this.unreadableAnswer("POST", `/${API_PREFIX}/${endpoint}`, res, fault);
|
|
2450
|
+
}
|
|
2451
|
+
return answer;
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* One page of the copies made of a method, in the order of the copies' ids, which says nothing
|
|
2455
|
+
* of when each was made — `GET /v1/methods/{id}/copies`.
|
|
2456
|
+
*
|
|
2457
|
+
* Each row is the `MethodCopyMarker` the original keeps of a copy: the copy's id, the
|
|
2458
|
+
* destination's id, the member who made it and when. The destination is named by its id alone,
|
|
2459
|
+
* never its name, so a surface shows a destination's name only from the reader's own
|
|
2460
|
+
* memberships, and a copy in an organization the reader does not belong to stays a bare id. A
|
|
2461
|
+
* marker stays when its copy is erased, and the copy then reads as not found. Pass the page's
|
|
2462
|
+
* `nextCursor` back as `cursor` to continue; `null` is the last page. A method never copied,
|
|
2463
|
+
* and a copy itself, answer an empty page. The listing is open to every member of the method's
|
|
2464
|
+
* organization, a runner included, as every read of a method is.
|
|
2465
|
+
*
|
|
2466
|
+
* Takes a bare catalog id, as `getMethod` does. Throws `ApiResponseError`: `404` `not_found` for
|
|
2467
|
+
* an unknown method, `409` `method_being_deleted`, `400` `malformed_request` for a cursor the
|
|
2468
|
+
* listing did not issue for this method, `422` for a `limit` outside 1 to 100. A page whose
|
|
2469
|
+
* `items` is not an array or whose `next_cursor` is neither a string nor `null` is an answer the
|
|
2470
|
+
* SDK cannot read, thrown as an `ApiResponseError` too.
|
|
2471
|
+
*/
|
|
2472
|
+
async listMethodCopies(methodId, query = {}) {
|
|
2473
|
+
const search = productQuery({ limit: query.limit, cursor: query.cursor });
|
|
2474
|
+
const endpoint = `${methodPath(methodId, "listMethodCopies")}/copies`;
|
|
2475
|
+
return this.requestPage(`${endpoint}${search}`);
|
|
2476
|
+
}
|
|
2350
2477
|
/**
|
|
2351
2478
|
* Erase a method and everything it produced — `DELETE /v1/methods/{id}`.
|
|
2352
2479
|
*
|
|
@@ -3802,7 +3929,7 @@ function assertLinkageMethodIdIsBare(options, treeForms) {
|
|
|
3802
3929
|
function nonEmptyString(value) {
|
|
3803
3930
|
return value != null && value.length > 0 ? value : undefined;
|
|
3804
3931
|
}
|
|
3805
|
-
/** The request header the hosted API reads a
|
|
3932
|
+
/** The request header the hosted API reads a request's idempotency key from. */
|
|
3806
3933
|
const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
|
|
3807
3934
|
/**
|
|
3808
3935
|
* The longest idempotency key a start sends. It is room for any generated key, and keeps the
|
|
@@ -3817,21 +3944,22 @@ const MAX_IDEMPOTENCY_KEY_LENGTH = 255;
|
|
|
3817
3944
|
*/
|
|
3818
3945
|
const VISIBLE_ASCII = /^[\x21-\x7E]+$/;
|
|
3819
3946
|
/**
|
|
3820
|
-
* The idempotency key a start sends, or `undefined` when the options carry none
|
|
3821
|
-
* `null`). A key that is not a string of 1 to 255 visible ASCII characters is refused
|
|
3822
|
-
* `RequestArgumentError` rather than dropped, an empty one included, since a
|
|
3823
|
-
* the key its caller gave would lose the protection the caller asked for without a word.
|
|
3824
|
-
*
|
|
3825
|
-
*
|
|
3947
|
+
* The idempotency key a start or a method copy sends, or `undefined` when the options carry none
|
|
3948
|
+
* (absent or `null`). A key that is not a string of 1 to 255 visible ASCII characters is refused
|
|
3949
|
+
* with a `RequestArgumentError` rather than dropped, an empty one included, since a request sent
|
|
3950
|
+
* without the key its caller gave would lose the protection the caller asked for without a word.
|
|
3951
|
+
* `request` says what the request does, as the refusal's last sentence names it ("start the run",
|
|
3952
|
+
* "copy the method"). The message never quotes the key, which a caller may derive from something
|
|
3953
|
+
* private. The Python SDK holds a key to the same rule, in the same words.
|
|
3826
3954
|
*/
|
|
3827
|
-
function idempotencyKeyOf(options) {
|
|
3955
|
+
function idempotencyKeyOf(options, request = "start the run") {
|
|
3828
3956
|
const key = options.idempotencyKey;
|
|
3829
3957
|
if (key == null)
|
|
3830
3958
|
return undefined;
|
|
3831
3959
|
const fault = idempotencyKeyFault(key);
|
|
3832
3960
|
if (fault !== undefined) {
|
|
3833
3961
|
throw new RequestArgumentError(`The idempotency key must be 1 to ${MAX_IDEMPOTENCY_KEY_LENGTH} visible ASCII characters ` +
|
|
3834
|
-
`(\`!\` to \`~\`), and this one ${fault}. Leave it out to
|
|
3962
|
+
`(\`!\` to \`~\`), and this one ${fault}. Leave it out to ${request} without one.`);
|
|
3835
3963
|
}
|
|
3836
3964
|
return key;
|
|
3837
3965
|
}
|