primitive-admin 1.0.60 → 1.0.62
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 +125 -69
- package/assets/skill/skills/primitive-platform/SKILL.md +336 -37
- package/dist/bin/primitive.js +29 -14
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.js +22 -8
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.js +96 -16
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.js +49 -188
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +50 -155
- package/dist/src/commands/blob-buckets.js.map +1 -1
- package/dist/src/commands/collection-type-configs.js +14 -100
- package/dist/src/commands/collection-type-configs.js.map +1 -1
- package/dist/src/commands/collections.js +47 -8
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/config.d.ts +46 -0
- package/dist/src/commands/config.js +479 -0
- package/dist/src/commands/config.js.map +1 -0
- package/dist/src/commands/connections.d.ts +2 -0
- package/dist/src/commands/connections.js +100 -0
- package/dist/src/commands/connections.js.map +1 -0
- package/dist/src/commands/cron-triggers.js +84 -176
- package/dist/src/commands/cron-triggers.js.map +1 -1
- package/dist/src/commands/database-type-configs.d.ts +2 -0
- package/dist/src/commands/database-type-configs.js +171 -0
- package/dist/src/commands/database-type-configs.js.map +1 -0
- package/dist/src/commands/database-types.js +4 -4
- package/dist/src/commands/database-types.js.map +1 -1
- package/dist/src/commands/databases.d.ts +63 -0
- package/dist/src/commands/databases.js +649 -87
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.js +869 -12
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.js +18 -125
- package/dist/src/commands/email-templates.js.map +1 -1
- package/dist/src/commands/env.d.ts +12 -1
- package/dist/src/commands/env.js +98 -25
- package/dist/src/commands/env.js.map +1 -1
- package/dist/src/commands/feature-flags.d.ts +14 -0
- package/dist/src/commands/feature-flags.js +116 -0
- package/dist/src/commands/feature-flags.js.map +1 -0
- package/dist/src/commands/group-type-configs.js +14 -109
- package/dist/src/commands/group-type-configs.js.map +1 -1
- package/dist/src/commands/init.d.ts +9 -1
- package/dist/src/commands/init.js +1058 -279
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.js +136 -421
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/metadata-category-configs.d.ts +12 -0
- package/dist/src/commands/metadata-category-configs.js +112 -0
- package/dist/src/commands/metadata-category-configs.js.map +1 -0
- package/dist/src/commands/metadata.js +33 -141
- package/dist/src/commands/metadata.js.map +1 -1
- package/dist/src/commands/prompts.js +206 -563
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.d.ts +1 -1
- package/dist/src/commands/rule-sets.js +16 -132
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/scripts.js +67 -234
- package/dist/src/commands/scripts.js.map +1 -1
- package/dist/src/commands/sessions.d.ts +2 -0
- package/dist/src/commands/sessions.js +75 -0
- package/dist/src/commands/sessions.js.map +1 -0
- package/dist/src/commands/skill.js +2 -2
- package/dist/src/commands/skill.js.map +1 -1
- package/dist/src/commands/sync-app-settings.d.ts +113 -70
- package/dist/src/commands/sync-app-settings.js +193 -242
- package/dist/src/commands/sync-app-settings.js.map +1 -1
- package/dist/src/commands/sync.d.ts +1932 -49
- package/dist/src/commands/sync.js +9584 -1725
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.js +8 -2
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.js +92 -1
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/vars.js +44 -58
- package/dist/src/commands/vars.js.map +1 -1
- package/dist/src/commands/webhooks.js +318 -156
- package/dist/src/commands/webhooks.js.map +1 -1
- package/dist/src/commands/workflows.d.ts +49 -12
- package/dist/src/commands/workflows.js +652 -1043
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/access-rule-display.d.ts +21 -0
- package/dist/src/lib/access-rule-display.js +34 -0
- package/dist/src/lib/access-rule-display.js.map +1 -0
- package/dist/src/lib/api-client.d.ts +630 -61
- package/dist/src/lib/api-client.js +676 -116
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +180 -25
- package/dist/src/lib/app-settings-descriptor.js +403 -82
- package/dist/src/lib/app-settings-descriptor.js.map +1 -1
- package/dist/src/lib/canonical-json.d.ts +12 -0
- package/dist/src/lib/canonical-json.js +35 -0
- package/dist/src/lib/canonical-json.js.map +1 -0
- package/dist/src/lib/cli-manifest.d.ts +8 -0
- package/dist/src/lib/cli-manifest.js +1 -0
- package/dist/src/lib/cli-manifest.js.map +1 -1
- package/dist/src/lib/codegen-shared/generatedFiles.d.ts +6 -2
- package/dist/src/lib/codegen-shared/generatedFiles.js +9 -2
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +6 -6
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +8 -8
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -1
- package/dist/src/lib/config-object-descriptor.d.ts +127 -0
- package/dist/src/lib/config-object-descriptor.js +658 -0
- package/dist/src/lib/config-object-descriptor.js.map +1 -0
- package/dist/src/lib/config-payload.d.ts +85 -0
- package/dist/src/lib/config-payload.js +116 -0
- package/dist/src/lib/config-payload.js.map +1 -0
- package/dist/src/lib/config-surface.d.ts +130 -0
- package/dist/src/lib/config-surface.js +300 -0
- package/dist/src/lib/config-surface.js.map +1 -0
- package/dist/src/lib/credentials-store.d.ts +10 -0
- package/dist/src/lib/credentials-store.js +23 -0
- package/dist/src/lib/credentials-store.js.map +1 -1
- package/dist/src/lib/data-input.d.ts +23 -0
- package/dist/src/lib/data-input.js +50 -0
- package/dist/src/lib/data-input.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTemplates.js +1 -1
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTypeIR.d.ts +1 -1
- package/dist/src/lib/env-resolver-core.d.ts +147 -0
- package/dist/src/lib/env-resolver-core.js +265 -0
- package/dist/src/lib/env-resolver-core.js.map +1 -0
- package/dist/src/lib/env-resolver.d.ts +28 -6
- package/dist/src/lib/env-resolver.js +44 -32
- package/dist/src/lib/env-resolver.js.map +1 -1
- package/dist/src/lib/generated-allowlist.js +13 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +682 -0
- package/dist/src/lib/generated-config-surfaces.js +4058 -0
- package/dist/src/lib/generated-config-surfaces.js.map +1 -0
- package/dist/src/lib/generated-template-lint.d.ts +212 -0
- package/dist/src/lib/generated-template-lint.js +624 -0
- package/dist/src/lib/generated-template-lint.js.map +1 -0
- package/dist/src/lib/init-adopt.d.ts +16 -0
- package/dist/src/lib/init-adopt.js +34 -0
- package/dist/src/lib/init-adopt.js.map +1 -0
- package/dist/src/lib/init-assets.d.ts +39 -0
- package/dist/src/lib/init-assets.js +97 -0
- package/dist/src/lib/init-assets.js.map +1 -0
- package/dist/src/lib/init-config.d.ts +44 -5
- package/dist/src/lib/init-config.js +80 -7
- package/dist/src/lib/init-config.js.map +1 -1
- package/dist/src/lib/init-ios-links.d.ts +50 -0
- package/dist/src/lib/init-ios-links.js +153 -0
- package/dist/src/lib/init-ios-links.js.map +1 -0
- package/dist/src/lib/init-plan.d.ts +80 -0
- package/dist/src/lib/init-plan.js +95 -0
- package/dist/src/lib/init-plan.js.map +1 -0
- package/dist/src/lib/init-production-env.d.ts +48 -0
- package/dist/src/lib/init-production-env.js +59 -0
- package/dist/src/lib/init-production-env.js.map +1 -0
- package/dist/src/lib/init-schema.d.ts +74 -0
- package/dist/src/lib/init-schema.js +358 -0
- package/dist/src/lib/init-schema.js.map +1 -0
- package/dist/src/lib/init-xcode.d.ts +33 -0
- package/dist/src/lib/init-xcode.js +114 -0
- package/dist/src/lib/init-xcode.js.map +1 -0
- package/dist/src/lib/integration-request-config.d.ts +30 -0
- package/dist/src/lib/integration-request-config.js +145 -0
- package/dist/src/lib/integration-request-config.js.map +1 -0
- package/dist/src/lib/local-state.d.ts +55 -0
- package/dist/src/lib/local-state.js +167 -0
- package/dist/src/lib/local-state.js.map +1 -0
- package/dist/src/lib/log-inspection.d.ts +568 -0
- package/dist/src/lib/log-inspection.js +639 -0
- package/dist/src/lib/log-inspection.js.map +1 -0
- package/dist/src/lib/migration-nag.d.ts +2 -2
- package/dist/src/lib/migration-nag.js +3 -3
- package/dist/src/lib/object-status-filter.d.ts +22 -0
- package/dist/src/lib/object-status-filter.js +45 -0
- package/dist/src/lib/object-status-filter.js.map +1 -0
- package/dist/src/lib/output.d.ts +24 -1
- package/dist/src/lib/output.js +42 -1
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/package-manager.d.ts +140 -0
- package/dist/src/lib/package-manager.js +305 -0
- package/dist/src/lib/package-manager.js.map +1 -0
- package/dist/src/lib/paginate.d.ts +50 -0
- package/dist/src/lib/paginate.js +53 -0
- package/dist/src/lib/paginate.js.map +1 -1
- package/dist/src/lib/platform-owned.d.ts +29 -3
- package/dist/src/lib/platform-owned.js +42 -3
- package/dist/src/lib/platform-owned.js.map +1 -1
- package/dist/src/lib/project-config.js +13 -5
- package/dist/src/lib/project-config.js.map +1 -1
- package/dist/src/lib/query-operators.d.ts +1 -1
- package/dist/src/lib/query-operators.js +1 -1
- package/dist/src/lib/record-filter.d.ts +18 -0
- package/dist/src/lib/record-filter.js +55 -0
- package/dist/src/lib/record-filter.js.map +1 -0
- package/dist/src/lib/resolve-init-dev-port.d.ts +56 -0
- package/dist/src/lib/resolve-init-dev-port.js +55 -0
- package/dist/src/lib/resolve-init-dev-port.js.map +1 -0
- package/dist/src/lib/resolve-init-server.d.ts +64 -0
- package/dist/src/lib/resolve-init-server.js +77 -0
- package/dist/src/lib/resolve-init-server.js.map +1 -0
- package/dist/src/lib/resolve-platform.d.ts +43 -14
- package/dist/src/lib/resolve-platform.js +74 -12
- package/dist/src/lib/resolve-platform.js.map +1 -1
- package/dist/src/lib/run-status.d.ts +19 -0
- package/dist/src/lib/run-status.generated.d.ts +39 -0
- package/dist/src/lib/run-status.generated.js +66 -0
- package/dist/src/lib/run-status.generated.js.map +1 -0
- package/dist/src/lib/run-status.js +19 -0
- package/dist/src/lib/run-status.js.map +1 -0
- package/dist/src/lib/server-text-normalization.d.ts +51 -0
- package/dist/src/lib/server-text-normalization.js +90 -0
- package/dist/src/lib/server-text-normalization.js.map +1 -0
- package/dist/src/lib/server-url.d.ts +22 -0
- package/dist/src/lib/server-url.js +33 -0
- package/dist/src/lib/server-url.js.map +1 -0
- package/dist/src/lib/signing-secret-status.d.ts +81 -0
- package/dist/src/lib/signing-secret-status.js +116 -0
- package/dist/src/lib/signing-secret-status.js.map +1 -0
- package/dist/src/lib/skill-installer.d.ts +4 -2
- package/dist/src/lib/skill-installer.js +143 -12
- package/dist/src/lib/skill-installer.js.map +1 -1
- package/dist/src/lib/snapshots.d.ts +2 -2
- package/dist/src/lib/snapshots.js +2 -2
- package/dist/src/lib/swift-codegen/dbGenerator.d.ts +15 -2
- package/dist/src/lib/swift-codegen/dbGenerator.js +250 -18
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/swift-codegen/generator.d.ts +11 -2
- package/dist/src/lib/swift-codegen/generator.js +181 -7
- package/dist/src/lib/swift-codegen/generator.js.map +1 -1
- package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +7 -3
- package/dist/src/lib/swift-codegen/siblingSymbols.js +4 -1
- package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -1
- package/dist/src/lib/sync-dir-selector.d.ts +21 -0
- package/dist/src/lib/sync-dir-selector.js +30 -0
- package/dist/src/lib/sync-dir-selector.js.map +1 -0
- package/dist/src/lib/sync-paths.d.ts +39 -0
- package/dist/src/lib/sync-paths.js +70 -2
- package/dist/src/lib/sync-paths.js.map +1 -1
- package/dist/src/lib/sync-resource-types.d.ts +138 -31
- package/dist/src/lib/sync-resource-types.js +241 -47
- package/dist/src/lib/sync-resource-types.js.map +1 -1
- package/dist/src/lib/sync-selectors.d.ts +95 -0
- package/dist/src/lib/sync-selectors.js +228 -0
- package/dist/src/lib/sync-selectors.js.map +1 -0
- package/dist/src/lib/template.d.ts +62 -4
- package/dist/src/lib/template.js +147 -39
- package/dist/src/lib/template.js.map +1 -1
- package/dist/src/lib/test-case-keys.d.ts +29 -0
- package/dist/src/lib/test-case-keys.js +55 -0
- package/dist/src/lib/test-case-keys.js.map +1 -0
- package/dist/src/lib/test-case-variables.d.ts +15 -0
- package/dist/src/lib/test-case-variables.js +29 -0
- package/dist/src/lib/test-case-variables.js.map +1 -0
- package/dist/src/lib/toml-database-config.d.ts +4 -4
- package/dist/src/lib/toml-database-config.js +27 -34
- package/dist/src/lib/toml-database-config.js.map +1 -1
- package/dist/src/lib/toml-metadata-config.d.ts +9 -5
- package/dist/src/lib/toml-metadata-config.js +40 -45
- package/dist/src/lib/toml-metadata-config.js.map +1 -1
- package/dist/src/lib/toml-native-form.d.ts +1 -1
- package/dist/src/lib/toml-native-form.js +1 -1
- package/dist/src/lib/toml-params-validator.d.ts +1 -1
- package/dist/src/lib/toml-params-validator.js +1 -1
- package/dist/src/lib/toml-scalar-edit.d.ts +43 -0
- package/dist/src/lib/toml-scalar-edit.js +283 -0
- package/dist/src/lib/toml-scalar-edit.js.map +1 -0
- package/dist/src/lib/user-selector.d.ts +24 -0
- package/dist/src/lib/user-selector.js +33 -0
- package/dist/src/lib/user-selector.js.map +1 -0
- package/dist/src/lib/version-check.d.ts +25 -0
- package/dist/src/lib/version-check.js +77 -8
- package/dist/src/lib/version-check.js.map +1 -1
- package/dist/src/lib/watch.d.ts +121 -0
- package/dist/src/lib/watch.js +169 -0
- package/dist/src/lib/watch.js.map +1 -0
- package/dist/src/lib/workflow-apply.d.ts +69 -45
- package/dist/src/lib/workflow-apply.js +99 -72
- package/dist/src/lib/workflow-apply.js.map +1 -1
- package/dist/src/lib/workflow-codegen/generator.js +4 -4
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
- package/dist/src/lib/workflow-codegen/invokerIR.d.ts +11 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js +5 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -1
- package/dist/src/lib/workflow-config-apply.d.ts +70 -0
- package/dist/src/lib/workflow-config-apply.js +137 -0
- package/dist/src/lib/workflow-config-apply.js.map +1 -0
- package/dist/src/lib/workflow-config-sidecar.d.ts +63 -0
- package/dist/src/lib/workflow-config-sidecar.js +96 -0
- package/dist/src/lib/workflow-config-sidecar.js.map +1 -0
- package/dist/src/lib/workflow-defaults.d.ts +29 -0
- package/dist/src/lib/workflow-defaults.js +41 -0
- package/dist/src/lib/workflow-defaults.js.map +1 -0
- package/dist/src/lib/workflow-fragments.d.ts +23 -0
- package/dist/src/lib/workflow-fragments.js +229 -8
- package/dist/src/lib/workflow-fragments.js.map +1 -1
- package/dist/src/lib/workflow-include-preserve.d.ts +76 -0
- package/dist/src/lib/workflow-include-preserve.js +286 -0
- package/dist/src/lib/workflow-include-preserve.js.map +1 -0
- package/dist/src/lib/workflow-payload.d.ts +31 -20
- package/dist/src/lib/workflow-payload.js +100 -93
- package/dist/src/lib/workflow-payload.js.map +1 -1
- package/dist/src/lib/workflow-toml-validator.d.ts +80 -3
- package/dist/src/lib/workflow-toml-validator.js +231 -3
- package/dist/src/lib/workflow-toml-validator.js.map +1 -1
- package/dist/src/types/index.d.ts +78 -22
- package/dist/src/validators.d.ts +7 -7
- package/dist/src/validators.js +6 -6
- package/package.json +11 -6
- package/dist/src/commands/settings.d.ts +0 -15
- package/dist/src/commands/settings.js +0 -102
- package/dist/src/commands/settings.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadCredentials, saveCredentials, isTokenExpiringSoon, } from "./config.js";
|
|
2
2
|
import { fetchWithTLS } from "./fetch.js";
|
|
3
|
-
import { paginateAll } from "./paginate.js";
|
|
3
|
+
import { paginateAll, normalizeCliListEnvelope } from "./paginate.js";
|
|
4
4
|
import { RefreshError, refreshAdminCredentials, } from "./refresh-admin-credentials.js";
|
|
5
5
|
export class ApiError extends Error {
|
|
6
6
|
statusCode;
|
|
@@ -162,6 +162,45 @@ export class OpsExistError extends ApiError {
|
|
|
162
162
|
return typeof d?.opCount === "number" ? d.opCount : 0;
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Normalize a `databases operations execute` result to the CLI's one list
|
|
167
|
+
* envelope — but only when the result is a Durable Object query page
|
|
168
|
+
* (issue #2440).
|
|
169
|
+
*
|
|
170
|
+
* A registered operation is a list only sometimes. Of the shapes the server's
|
|
171
|
+
* operation dispatch can return, exactly two carry a top-level `data` array: a
|
|
172
|
+
* bare `query` and a `pipeline` whose `returnField` names a query step. A
|
|
173
|
+
* `count` (`{ count }`), an `aggregate` (`{ result }`), a mutation
|
|
174
|
+
* (`{ results }`), an `applyToQuery` (`{ matched, affected, failed, … }`) and a
|
|
175
|
+
* `returnField: "all"` pipeline (`{ steps: { … } }`) do not, so blanket
|
|
176
|
+
* normalization would corrupt them. This helper therefore tests the shape
|
|
177
|
+
* first and passes everything else through byte-for-byte — including the
|
|
178
|
+
* nested steps of a `returnField: "all"` pipeline, which are operation-defined
|
|
179
|
+
* objects the CLI must not reach into.
|
|
180
|
+
*
|
|
181
|
+
* Deliberately narrow, not a general-purpose predicate: the contract is
|
|
182
|
+
* specifically "an operations-execute result may be a DO query page".
|
|
183
|
+
* `executeDatabaseOperation()` is the only caller.
|
|
184
|
+
*
|
|
185
|
+
* The conversion itself is delegated to `normalizeCliListEnvelope()` so cursor
|
|
186
|
+
* handling, `prevCursor` dropping and `hasMore` derivation have exactly one
|
|
187
|
+
* implementation. That helper throws on an unrecognized shape — correct when
|
|
188
|
+
* the payload is supposed to be a list, wrong here, where the payload is the
|
|
189
|
+
* caller's own result — which is why the shape test has to run first. `_timing`
|
|
190
|
+
* (`--timing`) is re-attached afterwards, since it rides alongside the envelope
|
|
191
|
+
* rather than inside it.
|
|
192
|
+
*/
|
|
193
|
+
export function normalizeDatabaseOperationResult(raw) {
|
|
194
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
195
|
+
return raw;
|
|
196
|
+
const page = raw;
|
|
197
|
+
if (!Array.isArray(page.data))
|
|
198
|
+
return raw;
|
|
199
|
+
const envelope = normalizeCliListEnvelope(page);
|
|
200
|
+
if (page._timing !== undefined)
|
|
201
|
+
envelope._timing = page._timing;
|
|
202
|
+
return envelope;
|
|
203
|
+
}
|
|
165
204
|
export class ApiClient {
|
|
166
205
|
credentials = null;
|
|
167
206
|
constructor() {
|
|
@@ -313,7 +352,14 @@ export class ApiClient {
|
|
|
313
352
|
if (cursor)
|
|
314
353
|
qs.set("cursor", cursor);
|
|
315
354
|
const resp = await this.get(`/admin/api/admins/me/apps?${qs.toString()}`);
|
|
316
|
-
|
|
355
|
+
// The server now emits `items` with `apps` as a deprecation-window
|
|
356
|
+
// alias (#1316). Read `items` first so listings keep working once the
|
|
357
|
+
// alias is removed; fall back to the legacy `apps` key meanwhile.
|
|
358
|
+
return {
|
|
359
|
+
items: resp.items ??
|
|
360
|
+
(Array.isArray(resp.apps) ? resp.apps : []),
|
|
361
|
+
nextCursor: resp.nextCursor ?? resp.cursor,
|
|
362
|
+
};
|
|
317
363
|
});
|
|
318
364
|
return { apps };
|
|
319
365
|
}
|
|
@@ -371,6 +417,40 @@ export class ApiClient {
|
|
|
371
417
|
async removeUser(appId, userId) {
|
|
372
418
|
return this.delete(`/app/${appId}/api/users/${userId}`);
|
|
373
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* #2803 — take a user's access away, or give it back. Distinct from
|
|
422
|
+
* `removeUser`, which detaches the membership: this blocks the person's auth
|
|
423
|
+
* in the app, revokes their sessions and tokens, and is reversible. Both
|
|
424
|
+
* routes already existed; nothing on the CLI reached them.
|
|
425
|
+
*/
|
|
426
|
+
async setUserAvailability(appId, userId, action) {
|
|
427
|
+
return this.put(`/admin/api/apps/${appId}/users/${userId}/${action}`, {});
|
|
428
|
+
}
|
|
429
|
+
// ============================================
|
|
430
|
+
// FEATURE FLAGS (super-admin)
|
|
431
|
+
// ============================================
|
|
432
|
+
/**
|
|
433
|
+
* #2803 — the platform's feature flags. A server-owned admin toggle with a
|
|
434
|
+
* console but, until now, no CLI: the same "one control, both surfaces" rule
|
|
435
|
+
* the carrying types get.
|
|
436
|
+
*/
|
|
437
|
+
async listFeatureFlags() {
|
|
438
|
+
return this.get(`/admin/api/flags`);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* The flag itself, not the `{ flag }` envelope the route answers with —
|
|
442
|
+
* `feature-flags get` prints the key, the enabled state and the per-app
|
|
443
|
+
* overrides, and reading them off the envelope printed a blank row.
|
|
444
|
+
*/
|
|
445
|
+
async getFeatureFlag(flagKey) {
|
|
446
|
+
const result = await this.get(`/admin/api/flags/${flagKey}`);
|
|
447
|
+
return result?.flag ?? result;
|
|
448
|
+
}
|
|
449
|
+
/** Same envelope, same unwrap: `enable`/`disable` print the updated flag. */
|
|
450
|
+
async setFeatureFlagEnabled(flagKey, enabled) {
|
|
451
|
+
const result = await this.put(`/admin/api/flags/${flagKey}`, { enabled });
|
|
452
|
+
return result?.flag ?? result;
|
|
453
|
+
}
|
|
374
454
|
async updateUserRole(appId, userId, role) {
|
|
375
455
|
return this.put(`/app/${appId}/api/users/${userId}/role`, { role });
|
|
376
456
|
}
|
|
@@ -403,7 +483,16 @@ export class ApiClient {
|
|
|
403
483
|
// INVITATIONS
|
|
404
484
|
// ============================================
|
|
405
485
|
async listInvitations(appId, params) {
|
|
406
|
-
|
|
486
|
+
const result = await this.get(`/app/${appId}/api/invitations`, params);
|
|
487
|
+
// #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
|
|
488
|
+
// so the CLI keeps paginating against pre-#1316 servers.
|
|
489
|
+
const nextCursor = result?.nextCursor ?? result?.cursor;
|
|
490
|
+
return {
|
|
491
|
+
items: result?.items ?? [],
|
|
492
|
+
nextCursor,
|
|
493
|
+
hasMore: result?.hasMore ?? nextCursor != null,
|
|
494
|
+
cursor: nextCursor,
|
|
495
|
+
};
|
|
407
496
|
}
|
|
408
497
|
async createInvitation(appId, data) {
|
|
409
498
|
return this.post(`/app/${appId}/api/invitations`, data);
|
|
@@ -433,8 +522,9 @@ export class ApiClient {
|
|
|
433
522
|
async listDeferredGrants(appId, params) {
|
|
434
523
|
const result = await this.get(`/app/${appId}/api/deferred-grants`, params);
|
|
435
524
|
return {
|
|
436
|
-
grants: result?.grants ?? [],
|
|
525
|
+
grants: result?.grants ?? result?.items ?? [],
|
|
437
526
|
nextCursor: result?.nextCursor ?? null,
|
|
527
|
+
hasMore: result?.hasMore,
|
|
438
528
|
};
|
|
439
529
|
}
|
|
440
530
|
async revokeDeferredGrant(appId, deferredId, type) {
|
|
@@ -450,9 +540,19 @@ export class ApiClient {
|
|
|
450
540
|
nextCursor: result?.nextCursor ?? null,
|
|
451
541
|
};
|
|
452
542
|
}
|
|
543
|
+
/** Integration detail (#2631: `accessRule` is on the detail, not the list summary). */
|
|
453
544
|
async getIntegration(appId, integrationId) {
|
|
454
545
|
return this.get(`/admin/api/apps/${appId}/integrations/${integrationId}`);
|
|
455
546
|
}
|
|
547
|
+
/**
|
|
548
|
+
* An integration's named configs. Test cases pin a config by NAME so the
|
|
549
|
+
* sidecar is portable across apps (#2769); this is the map pull and push
|
|
550
|
+
* resolve that name through, the same way prompts/workflows/scripts do.
|
|
551
|
+
*/
|
|
552
|
+
async listIntegrationConfigs(appId, integrationId) {
|
|
553
|
+
const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/configs`);
|
|
554
|
+
return { items: result?.items ?? result ?? [] };
|
|
555
|
+
}
|
|
456
556
|
async createIntegration(appId, payload) {
|
|
457
557
|
return this.post(`/admin/api/apps/${appId}/integrations`, payload);
|
|
458
558
|
}
|
|
@@ -460,8 +560,15 @@ export class ApiClient {
|
|
|
460
560
|
const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
|
|
461
561
|
return this.patch(`/admin/api/apps/${appId}/integrations/${integrationId}`, body);
|
|
462
562
|
}
|
|
563
|
+
/**
|
|
564
|
+
* Delete an integration. Archives it by default; `{ hard: true }` destroys
|
|
565
|
+
* the row instead, which is what `config push --prune` passes (#2803) so a
|
|
566
|
+
* pruned integration stops holding its key. The positional boolean is still
|
|
567
|
+
* accepted for the existing callers.
|
|
568
|
+
*/
|
|
463
569
|
async deleteIntegration(appId, integrationId, hard) {
|
|
464
|
-
const
|
|
570
|
+
const isHard = typeof hard === "object" ? hard?.hard === true : hard === true;
|
|
571
|
+
const path = isHard
|
|
465
572
|
? `/admin/api/apps/${appId}/integrations/${integrationId}?hard=true`
|
|
466
573
|
: `/admin/api/apps/${appId}/integrations/${integrationId}`;
|
|
467
574
|
return this.delete(path);
|
|
@@ -477,19 +584,6 @@ export class ApiClient {
|
|
|
477
584
|
const result = await this.get(`/admin/api/apps/${appId}/workflows/runs/${runId}/integration-logs`, params);
|
|
478
585
|
return result?.items ?? [];
|
|
479
586
|
}
|
|
480
|
-
async listIntegrationSecrets(appId, integrationId, params) {
|
|
481
|
-
const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets`, params);
|
|
482
|
-
return result?.items ?? [];
|
|
483
|
-
}
|
|
484
|
-
async addIntegrationSecret(appId, integrationId, payload) {
|
|
485
|
-
const result = await this.post(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets`, payload);
|
|
486
|
-
return result?.secret ?? null;
|
|
487
|
-
}
|
|
488
|
-
async archiveIntegrationSecret(appId, integrationId, secretId) {
|
|
489
|
-
return this.patch(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets/${secretId}`, {
|
|
490
|
-
status: "inactive",
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
587
|
// ============================================
|
|
494
588
|
// APP SECRETS
|
|
495
589
|
// ============================================
|
|
@@ -566,19 +660,90 @@ export class ApiClient {
|
|
|
566
660
|
const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
|
|
567
661
|
return this.patch(`/admin/api/apps/${appId}/webhooks/${webhookId}`, body);
|
|
568
662
|
}
|
|
569
|
-
|
|
570
|
-
|
|
663
|
+
/**
|
|
664
|
+
* Delete a webhook. Archives it by default; `hard` hard-deletes the row
|
|
665
|
+
* instead (#2232), which is what frees a slot under the per-app webhook cap
|
|
666
|
+
* and releases the webhook key for reuse. Same `?hard=true` verb the admin
|
|
667
|
+
* integrations delete uses.
|
|
668
|
+
*/
|
|
669
|
+
async deleteWebhook(appId, webhookId, options) {
|
|
670
|
+
const query = options?.hard ? "?hard=true" : "";
|
|
671
|
+
return this.delete(`/admin/api/apps/${appId}/webhooks/${webhookId}${query}`);
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* #2803 — the operational verb pair for a webhook. It posts to the
|
|
675
|
+
* enable/disable route rather than PATCHing `status`, which is server-owned
|
|
676
|
+
* and refused on every configuration write path.
|
|
677
|
+
*/
|
|
678
|
+
async setWebhookAvailability(appId, webhookId, action) {
|
|
679
|
+
return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/${action}`, {});
|
|
680
|
+
}
|
|
681
|
+
/** #2803 — the operational verb pair for a prompt (admin API). */
|
|
682
|
+
async setPromptAvailability(appId, promptId, action) {
|
|
683
|
+
return this.post(`/admin/api/apps/${appId}/prompts/${promptId}/${action}`, {});
|
|
684
|
+
}
|
|
685
|
+
/** #2803 — the operational verb pair for an integration (admin API). */
|
|
686
|
+
async setIntegrationAvailability(appId, integrationId, action) {
|
|
687
|
+
return this.post(`/admin/api/apps/${appId}/integrations/${integrationId}/${action}`, {});
|
|
571
688
|
}
|
|
572
689
|
async rotateWebhookSecret(appId, webhookId, payload) {
|
|
573
690
|
return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/rotate-secret`, payload);
|
|
574
691
|
}
|
|
575
692
|
async listWebhookEvents(appId, webhookId, params) {
|
|
576
693
|
const result = await this.get(`/admin/api/apps/${appId}/webhooks/${webhookId}/events`, params);
|
|
577
|
-
|
|
694
|
+
// #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias.
|
|
695
|
+
const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
|
|
696
|
+
return {
|
|
697
|
+
items: result?.items ?? [],
|
|
698
|
+
hasMore: result?.hasMore ?? !!nextCursor,
|
|
699
|
+
nextCursor,
|
|
700
|
+
cursor: nextCursor,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
// ============================================
|
|
704
|
+
// CONNECTIONS INSPECTION (#1968)
|
|
705
|
+
// ============================================
|
|
706
|
+
/**
|
|
707
|
+
* List the live connections the server believes exist for exactly one of a
|
|
708
|
+
* document, user, or database. Read-only inspection over admin-api. Rows are
|
|
709
|
+
* per-connection-×-subscription bindings, so `connectionId` is not unique
|
|
710
|
+
* across rows. `params` on a database-subscription row is caller-defined
|
|
711
|
+
* bound CEL input — typed `unknown`.
|
|
712
|
+
*/
|
|
713
|
+
async listConnections(appId, selector, params) {
|
|
714
|
+
const result = await this.get(`/admin/api/apps/${appId}/connections`, {
|
|
715
|
+
...selector,
|
|
716
|
+
...params,
|
|
717
|
+
});
|
|
718
|
+
return {
|
|
719
|
+
items: result?.items ?? [],
|
|
720
|
+
nextCursor: result?.nextCursor ?? null,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
async listSessions(appId, userId, params) {
|
|
724
|
+
const result = await this.get(`/admin/api/apps/${appId}/sessions`, {
|
|
725
|
+
userId,
|
|
726
|
+
...params,
|
|
727
|
+
});
|
|
728
|
+
return {
|
|
729
|
+
items: result?.items ?? [],
|
|
730
|
+
nextCursor: result?.nextCursor ?? null,
|
|
731
|
+
};
|
|
578
732
|
}
|
|
579
733
|
async testWebhook(appId, webhookId, payload) {
|
|
580
734
|
return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/test`, payload || {});
|
|
581
735
|
}
|
|
736
|
+
/**
|
|
737
|
+
* Run a webhook's configured verifier over headers and a body captured from
|
|
738
|
+
* a real delivery, and report whether that signature checks out (#2445).
|
|
739
|
+
*
|
|
740
|
+
* `headers` is sent as ordered `[name, value]` pairs so a capture carrying a
|
|
741
|
+
* field name on more than one line survives the round trip. Nothing is
|
|
742
|
+
* delivered: no delivery record, no workflow run.
|
|
743
|
+
*/
|
|
744
|
+
async verifyWebhook(appId, webhookId, payload) {
|
|
745
|
+
return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/verify`, payload);
|
|
746
|
+
}
|
|
582
747
|
// ============================================
|
|
583
748
|
// NAMED LOCKS (#1518)
|
|
584
749
|
// ============================================
|
|
@@ -614,14 +779,21 @@ export class ApiClient {
|
|
|
614
779
|
async updateCronTrigger(appId, triggerId, payload) {
|
|
615
780
|
return this.put(`/app/${appId}/api/cron-triggers/${triggerId}`, payload);
|
|
616
781
|
}
|
|
617
|
-
|
|
618
|
-
|
|
782
|
+
/**
|
|
783
|
+
* Retire a cron trigger. The default soft-deletes (writes the `archived`
|
|
784
|
+
* tombstone); `{ hard: true }` destroys the row, which is what
|
|
785
|
+
* `config push --prune` passes (#2803) so a pruned trigger stops consuming
|
|
786
|
+
* the per-app cap and holding its key.
|
|
787
|
+
*/
|
|
788
|
+
async deleteCronTrigger(appId, triggerId, options = {}) {
|
|
789
|
+
const query = options.hard ? "?hard=true" : "";
|
|
790
|
+
return this.delete(`/app/${appId}/api/cron-triggers/${triggerId}${query}`);
|
|
619
791
|
}
|
|
620
|
-
async
|
|
621
|
-
return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/
|
|
792
|
+
async disableCronTrigger(appId, triggerId) {
|
|
793
|
+
return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/disable`, {});
|
|
622
794
|
}
|
|
623
|
-
async
|
|
624
|
-
return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/
|
|
795
|
+
async enableCronTrigger(appId, triggerId) {
|
|
796
|
+
return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/enable`, {});
|
|
625
797
|
}
|
|
626
798
|
async testCronTrigger(appId, triggerId) {
|
|
627
799
|
return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/test`, {});
|
|
@@ -669,6 +841,20 @@ export class ApiClient {
|
|
|
669
841
|
async deleteResourceMetadata(appId, resourceType, resourceId, category) {
|
|
670
842
|
return this.delete(`/app/${appId}/api/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/metadata/${encodeURIComponent(category)}`);
|
|
671
843
|
}
|
|
844
|
+
/**
|
|
845
|
+
* Reverse-resolve a resource by a category's unique metadata value (issue
|
|
846
|
+
* #2137). A miss is a success with `resourceId: null`, never an error. The
|
|
847
|
+
* CLI authenticates as a console admin, so the app-level owner/admin bypass
|
|
848
|
+
* skips readRule evaluation.
|
|
849
|
+
*/
|
|
850
|
+
async resolveResourceMetadata(appId, resourceType, category, key, value) {
|
|
851
|
+
return this.post(`/app/${appId}/api/metadata/resolve`, {
|
|
852
|
+
resourceType,
|
|
853
|
+
category,
|
|
854
|
+
key,
|
|
855
|
+
value,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
672
858
|
// ============================================
|
|
673
859
|
// PROMPTS
|
|
674
860
|
// ============================================
|
|
@@ -689,6 +875,12 @@ export class ApiClient {
|
|
|
689
875
|
const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
|
|
690
876
|
return this.patch(`/admin/api/apps/${appId}/prompts/${promptId}`, body);
|
|
691
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Retire a prompt, or destroy it.
|
|
880
|
+
*
|
|
881
|
+
* `hard` was always sent by both first-party clients and always ignored by
|
|
882
|
+
* the server; #2887 made it mean something — the plain call now archives.
|
|
883
|
+
*/
|
|
692
884
|
async deletePrompt(appId, promptId, hard) {
|
|
693
885
|
const path = hard
|
|
694
886
|
? `/admin/api/apps/${appId}/prompts/${promptId}?hard=true`
|
|
@@ -728,7 +920,7 @@ export class ApiClient {
|
|
|
728
920
|
// The `Script` model is the authoring surface for `script` workflow
|
|
729
921
|
// steps. A `Script` is a HEADER (name/description/activeConfigId); the
|
|
730
922
|
// Rhai body lives on versioned `ScriptConfig` rows resolved LIVE at run
|
|
731
|
-
// time. CLI `
|
|
923
|
+
// time. CLI `config push` reads `transforms/*.rhai` files and reconciles
|
|
732
924
|
// them: a new file → create script (mints a default config); a changed
|
|
733
925
|
// file → create a new config + activate it (zero fan-out — referencing
|
|
734
926
|
// workflows pick up the new body on their next run).
|
|
@@ -761,10 +953,28 @@ export class ApiClient {
|
|
|
761
953
|
* workflows pick up the new body on their next run with no fan-out. The
|
|
762
954
|
* config name is unique per script, so we mint a timestamped name.
|
|
763
955
|
*/
|
|
764
|
-
async pushScriptBody(appId, scriptId, body
|
|
956
|
+
async pushScriptBody(appId, scriptId, body,
|
|
957
|
+
/**
|
|
958
|
+
* The active state the caller believes holds right now (#2731 B10).
|
|
959
|
+
* Creating an inactive config races nothing, so the guard sits on the
|
|
960
|
+
* ACTIVATION step: if the active config has moved since the caller read it,
|
|
961
|
+
* the server rejects with a 409 instead of overwriting a concurrent script
|
|
962
|
+
* change. It takes BOTH halves of "what is active", because a config's body
|
|
963
|
+
* can be edited in place without the active id changing — the id alone
|
|
964
|
+
* would let such an edit through. Omitted (e.g. `--force`) means activate
|
|
965
|
+
* unconditionally.
|
|
966
|
+
*/
|
|
967
|
+
expectedActive) {
|
|
765
968
|
const configName = `sync-${Date.now()}`;
|
|
766
969
|
const config = await this.post(`/admin/api/apps/${appId}/scripts/${scriptId}/configs`, { configName, body });
|
|
767
|
-
|
|
970
|
+
const activateBody = {};
|
|
971
|
+
if (expectedActive?.configId) {
|
|
972
|
+
activateBody.expectedActiveConfigId = expectedActive.configId;
|
|
973
|
+
}
|
|
974
|
+
if (expectedActive?.modifiedAt) {
|
|
975
|
+
activateBody.expectedActiveModifiedAt = expectedActive.modifiedAt;
|
|
976
|
+
}
|
|
977
|
+
await this.post(`/admin/api/apps/${appId}/scripts/${scriptId}/configs/${config.configId}/activate`, activateBody);
|
|
768
978
|
return config;
|
|
769
979
|
}
|
|
770
980
|
/**
|
|
@@ -784,30 +994,54 @@ export class ApiClient {
|
|
|
784
994
|
// ============================================
|
|
785
995
|
// BLOCK TEST CASES (Prompts, Integrations, Workflows)
|
|
786
996
|
// ============================================
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
997
|
+
/**
|
|
998
|
+
* One page of a block's test cases. The endpoint paginates (default 50, cap
|
|
999
|
+
* 100) and reports `nextCursor`; callers that reconcile the sync tree must
|
|
1000
|
+
* drain every page before deleting or classifying anything (#2769).
|
|
1001
|
+
*/
|
|
1002
|
+
async listTestCases(appId, blockType, blockId, params = {}) {
|
|
1003
|
+
const query = new URLSearchParams();
|
|
1004
|
+
if (params.limit !== undefined)
|
|
1005
|
+
query.set("limit", String(params.limit));
|
|
1006
|
+
if (params.cursor)
|
|
1007
|
+
query.set("cursor", params.cursor);
|
|
1008
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
1009
|
+
const result = await this.get(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases${suffix}`);
|
|
1010
|
+
return {
|
|
1011
|
+
items: result?.items ?? result ?? [],
|
|
1012
|
+
nextCursor: result?.nextCursor ?? undefined,
|
|
1013
|
+
};
|
|
790
1014
|
}
|
|
791
1015
|
async getTestCase(appId, blockType, blockId, testCaseId) {
|
|
792
1016
|
return this.get(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases/${testCaseId}`);
|
|
793
1017
|
}
|
|
794
1018
|
async createTestCase(appId, blockType, blockId, payload) {
|
|
795
|
-
//
|
|
1019
|
+
// #2769 — the JSON fields travel PARSED. The handler stringifies them for
|
|
1020
|
+
// storage (`createBlockTestCase`) and `serializeBlockTestCase` parses them
|
|
1021
|
+
// on the way back out, so a client that stringifies first stores a JSON
|
|
1022
|
+
// string of a JSON string and reads back a quoted blob.
|
|
796
1023
|
const serverPayload = {
|
|
797
1024
|
name: payload.name,
|
|
798
|
-
inputVariables:
|
|
1025
|
+
inputVariables: payload.inputVariables,
|
|
799
1026
|
configId: payload.configId,
|
|
800
1027
|
evaluatorPromptId: payload.evaluatorPromptId,
|
|
801
1028
|
evaluatorConfigId: payload.evaluatorConfigId,
|
|
802
1029
|
};
|
|
1030
|
+
// #2896 — only when the caller has one: an older server rejects the key
|
|
1031
|
+
// outright, and push reads that 400 to learn what it is talking to.
|
|
1032
|
+
if (payload.key !== undefined)
|
|
1033
|
+
serverPayload.key = payload.key;
|
|
1034
|
+
if (payload.description !== undefined) {
|
|
1035
|
+
serverPayload.description = payload.description;
|
|
1036
|
+
}
|
|
803
1037
|
if (payload.expectedOutputPattern) {
|
|
804
1038
|
serverPayload.expectedOutputPattern = payload.expectedOutputPattern;
|
|
805
1039
|
}
|
|
806
1040
|
if (payload.expectedOutputContains) {
|
|
807
|
-
serverPayload.expectedOutputContains =
|
|
1041
|
+
serverPayload.expectedOutputContains = payload.expectedOutputContains;
|
|
808
1042
|
}
|
|
809
1043
|
if (payload.expectedJsonSubset) {
|
|
810
|
-
serverPayload.expectedJsonSubset =
|
|
1044
|
+
serverPayload.expectedJsonSubset = payload.expectedJsonSubset;
|
|
811
1045
|
}
|
|
812
1046
|
return this.post(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases`, serverPayload);
|
|
813
1047
|
}
|
|
@@ -815,21 +1049,23 @@ export class ApiClient {
|
|
|
815
1049
|
const serverPayload = {};
|
|
816
1050
|
if (payload.name !== undefined)
|
|
817
1051
|
serverPayload.name = payload.name;
|
|
1052
|
+
if (payload.description !== undefined) {
|
|
1053
|
+
serverPayload.description = payload.description;
|
|
1054
|
+
}
|
|
1055
|
+
// #2769 — parsed on the wire (see `createTestCase`), and an explicit `null`
|
|
1056
|
+
// is how the authored file clears a field: the handler only touches keys
|
|
1057
|
+
// that are present, so an omitted key retains the previous value.
|
|
818
1058
|
if (payload.inputVariables !== undefined) {
|
|
819
|
-
serverPayload.inputVariables =
|
|
1059
|
+
serverPayload.inputVariables = payload.inputVariables;
|
|
820
1060
|
}
|
|
821
1061
|
if (payload.expectedOutputPattern !== undefined) {
|
|
822
1062
|
serverPayload.expectedOutputPattern = payload.expectedOutputPattern;
|
|
823
1063
|
}
|
|
824
1064
|
if (payload.expectedOutputContains !== undefined) {
|
|
825
|
-
serverPayload.expectedOutputContains = payload.expectedOutputContains
|
|
826
|
-
? JSON.stringify(payload.expectedOutputContains)
|
|
827
|
-
: null;
|
|
1065
|
+
serverPayload.expectedOutputContains = payload.expectedOutputContains;
|
|
828
1066
|
}
|
|
829
1067
|
if (payload.expectedJsonSubset !== undefined) {
|
|
830
|
-
serverPayload.expectedJsonSubset = payload.expectedJsonSubset
|
|
831
|
-
? JSON.stringify(payload.expectedJsonSubset)
|
|
832
|
-
: null;
|
|
1068
|
+
serverPayload.expectedJsonSubset = payload.expectedJsonSubset;
|
|
833
1069
|
}
|
|
834
1070
|
if (payload.configId !== undefined)
|
|
835
1071
|
serverPayload.configId = payload.configId;
|
|
@@ -839,6 +1075,8 @@ export class ApiClient {
|
|
|
839
1075
|
if (payload.evaluatorConfigId !== undefined) {
|
|
840
1076
|
serverPayload.evaluatorConfigId = payload.evaluatorConfigId;
|
|
841
1077
|
}
|
|
1078
|
+
if (payload.key !== undefined)
|
|
1079
|
+
serverPayload.key = payload.key;
|
|
842
1080
|
return this.patch(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases/${testCaseId}`, serverPayload);
|
|
843
1081
|
}
|
|
844
1082
|
async deleteTestCase(appId, blockType, blockId, testCaseId) {
|
|
@@ -935,15 +1173,33 @@ export class ApiClient {
|
|
|
935
1173
|
const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
|
|
936
1174
|
return this.patch(`/admin/api/apps/${appId}/workflows/${workflowId}`, body);
|
|
937
1175
|
}
|
|
938
|
-
|
|
939
|
-
|
|
1176
|
+
/**
|
|
1177
|
+
* Retire a workflow, or destroy it (#2887).
|
|
1178
|
+
*
|
|
1179
|
+
* The plain call ARCHIVES: the definition row stays, so its runs, revisions
|
|
1180
|
+
* and test cases keep resolving. `{ hard: true }` performs the full cascade
|
|
1181
|
+
* and is what releases the `workflowKey` — the flag `config push --prune`
|
|
1182
|
+
* sends, for the same reason it sends it for webhooks and integrations.
|
|
1183
|
+
*/
|
|
1184
|
+
async deleteWorkflow(appId, workflowId, options) {
|
|
1185
|
+
const path = `/admin/api/apps/${appId}/workflows/${workflowId}`;
|
|
1186
|
+
return this.delete(options?.hard ? `${path}?hard=true` : path);
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* #2645 criterion 9 — the operational toggle. Deliberately NOT the config
|
|
1190
|
+
* PATCH: `config push` owns every configuration field, and taking a workflow
|
|
1191
|
+
* out of service must not touch one, or an operator's action would show up as
|
|
1192
|
+
* drift against the repo. Same route shape as `cron-triggers pause|resume`.
|
|
1193
|
+
*/
|
|
1194
|
+
async disableWorkflow(appId, workflowId) {
|
|
1195
|
+
return this.post(`/app/${appId}/api/workflows/${workflowId}/disable`, {});
|
|
1196
|
+
}
|
|
1197
|
+
async enableWorkflow(appId, workflowId) {
|
|
1198
|
+
return this.post(`/app/${appId}/api/workflows/${workflowId}/enable`, {});
|
|
940
1199
|
}
|
|
941
1200
|
async updateWorkflowDraft(appId, workflowId, payload) {
|
|
942
1201
|
return this.put(`/admin/api/apps/${appId}/workflows/${workflowId}/draft`, payload);
|
|
943
1202
|
}
|
|
944
|
-
async publishWorkflow(appId, workflowId) {
|
|
945
|
-
return this.post(`/admin/api/apps/${appId}/workflows/${workflowId}/publish`, {});
|
|
946
|
-
}
|
|
947
1203
|
async previewWorkflow(appId, workflowId, payload) {
|
|
948
1204
|
return this.post(`/admin/api/apps/${appId}/workflows/${workflowId}/preview`, payload);
|
|
949
1205
|
}
|
|
@@ -952,9 +1208,31 @@ export class ApiClient {
|
|
|
952
1208
|
}
|
|
953
1209
|
async listWorkflowRuns(appId, workflowId, params) {
|
|
954
1210
|
const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs`, params);
|
|
1211
|
+
// #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias.
|
|
1212
|
+
const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
|
|
955
1213
|
return {
|
|
956
1214
|
items: result?.items ?? [],
|
|
957
|
-
|
|
1215
|
+
hasMore: result?.hasMore,
|
|
1216
|
+
nextCursor,
|
|
1217
|
+
cursor: nextCursor,
|
|
1218
|
+
resumeAfter: result?.resumeAfter,
|
|
1219
|
+
scanned: typeof result?.scanned === "number" ? result.scanned : undefined,
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Workflow runs started by one app user, across every workflow (#1967).
|
|
1224
|
+
* Backed by `GET /admin/api/apps/{appId}/users/{userId}/workflow-runs`,
|
|
1225
|
+
* which resolves the app user before querying the `runsByUser` index.
|
|
1226
|
+
*/
|
|
1227
|
+
async listUserWorkflowRuns(appId, userId, params) {
|
|
1228
|
+
const result = await this.get(`/admin/api/apps/${appId}/users/${encodeURIComponent(userId)}/workflow-runs`, params);
|
|
1229
|
+
const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
|
|
1230
|
+
return {
|
|
1231
|
+
items: result?.items ?? [],
|
|
1232
|
+
hasMore: result?.hasMore,
|
|
1233
|
+
nextCursor,
|
|
1234
|
+
cursor: nextCursor,
|
|
1235
|
+
scanned: typeof result?.scanned === "number" ? result.scanned : undefined,
|
|
958
1236
|
};
|
|
959
1237
|
}
|
|
960
1238
|
async getWorkflowRunStatus(appId, workflowId, runId) {
|
|
@@ -964,12 +1242,9 @@ export class ApiClient {
|
|
|
964
1242
|
const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/steps`);
|
|
965
1243
|
return { items: result?.items ?? [] };
|
|
966
1244
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
async getTopWorkflows(appId, params) {
|
|
971
|
-
return this.get(`/app/${appId}/api/analytics/workflows/top`, params);
|
|
972
|
-
}
|
|
1245
|
+
// Workflow analytics have one client path, `getAnalyticsTopWorkflows`
|
|
1246
|
+
// (issue #2766): `getTopWorkflows` was a second name for the same endpoint,
|
|
1247
|
+
// and `getWorkflowAnalytics` called a route the server never registered.
|
|
973
1248
|
// ============================================
|
|
974
1249
|
// WORKFLOW CONFIGURATIONS
|
|
975
1250
|
// ============================================
|
|
@@ -1040,6 +1315,9 @@ export class ApiClient {
|
|
|
1040
1315
|
async getAnalyticsEventsGrouped(appId, params) {
|
|
1041
1316
|
return this.get(`/app/${appId}/api/analytics/events/grouped`, params);
|
|
1042
1317
|
}
|
|
1318
|
+
async getAnalyticsErrorsGroups(appId, params) {
|
|
1319
|
+
return this.get(`/app/${appId}/api/analytics/errors/groups`, params);
|
|
1320
|
+
}
|
|
1043
1321
|
async getAnalyticsTopWorkflows(appId, params) {
|
|
1044
1322
|
return this.get(`/app/${appId}/api/analytics/workflows/top`, params);
|
|
1045
1323
|
}
|
|
@@ -1051,9 +1329,12 @@ export class ApiClient {
|
|
|
1051
1329
|
// ============================================
|
|
1052
1330
|
async listAllAdmins(params) {
|
|
1053
1331
|
const result = await this.get("/admin/api/admins", params);
|
|
1332
|
+
// #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias.
|
|
1333
|
+
const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
|
|
1054
1334
|
return {
|
|
1055
|
-
admins: result?.admins ?? [],
|
|
1056
|
-
|
|
1335
|
+
admins: result?.admins ?? result?.items ?? [],
|
|
1336
|
+
nextCursor,
|
|
1337
|
+
cursor: nextCursor,
|
|
1057
1338
|
};
|
|
1058
1339
|
}
|
|
1059
1340
|
async searchAdminByEmail(email) {
|
|
@@ -1121,21 +1402,6 @@ export class ApiClient {
|
|
|
1121
1402
|
return this.delete(`/admin/api/catalog/integrations/${catalogId}`);
|
|
1122
1403
|
}
|
|
1123
1404
|
// ============================================
|
|
1124
|
-
// LLM UTILITIES
|
|
1125
|
-
// ============================================
|
|
1126
|
-
async listLlmModels(provider = "openrouter") {
|
|
1127
|
-
return this.get("/admin/api/llm/models", { provider });
|
|
1128
|
-
}
|
|
1129
|
-
async generatePrompt(payload) {
|
|
1130
|
-
return this.post("/admin/api/llm/generate-prompt", payload);
|
|
1131
|
-
}
|
|
1132
|
-
async generateEvaluator(payload) {
|
|
1133
|
-
return this.post("/admin/api/llm/generate-evaluator", payload);
|
|
1134
|
-
}
|
|
1135
|
-
async generateWorkflowEvaluator(payload) {
|
|
1136
|
-
return this.post("/admin/api/llm/generate-evaluator-workflow", payload);
|
|
1137
|
-
}
|
|
1138
|
-
// ============================================
|
|
1139
1405
|
// BATCH TEST EXECUTION
|
|
1140
1406
|
// ============================================
|
|
1141
1407
|
async startBatchTests(appId, blockType, blockId, payload) {
|
|
@@ -1162,8 +1428,17 @@ export class ApiClient {
|
|
|
1162
1428
|
async getEmailTemplate(appId, emailType) {
|
|
1163
1429
|
return this.get(`/admin/api/apps/${appId}/email-templates/${emailType}`);
|
|
1164
1430
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1431
|
+
/**
|
|
1432
|
+
* Create or replace an email-template override.
|
|
1433
|
+
*
|
|
1434
|
+
* `expectedModifiedAt` is the optimistic-concurrency token every sibling
|
|
1435
|
+
* config endpoint already takes (#2731 B10). Optional and additive: when
|
|
1436
|
+
* omitted the write is unconditional, which is how `--force` and every
|
|
1437
|
+
* pre-existing caller keep working; when supplied and stale, the server
|
|
1438
|
+
* answers 409 rather than silently overwriting a concurrent edit.
|
|
1439
|
+
*/
|
|
1440
|
+
async upsertEmailTemplate(appId, emailType, payload, expectedModifiedAt) {
|
|
1441
|
+
return this.put(`/admin/api/apps/${appId}/email-templates/${emailType}`, expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload);
|
|
1167
1442
|
}
|
|
1168
1443
|
async deleteEmailTemplate(appId, emailType) {
|
|
1169
1444
|
return this.delete(`/admin/api/apps/${appId}/email-templates/${emailType}`);
|
|
@@ -1190,8 +1465,42 @@ export class ApiClient {
|
|
|
1190
1465
|
// ============================================
|
|
1191
1466
|
// DATABASES
|
|
1192
1467
|
// ============================================
|
|
1193
|
-
|
|
1194
|
-
|
|
1468
|
+
/**
|
|
1469
|
+
* List an app's databases, following the cursor to the end.
|
|
1470
|
+
*
|
|
1471
|
+
* The endpoint returns the `{ items, hasMore, nextCursor }` envelope and
|
|
1472
|
+
* caps a page at 100 (#1958), so `databases list` needs the follow-up pages
|
|
1473
|
+
* to show every database. `paginateAll` carries the repeat-cursor and
|
|
1474
|
+
* max-page guards, so a server that hands out a non-advancing cursor fails
|
|
1475
|
+
* loudly instead of looping forever.
|
|
1476
|
+
*
|
|
1477
|
+
* `options.owner` narrows the result to the databases that user created; the
|
|
1478
|
+
* server resolves it through the `databasesByCreator` index (#1965).
|
|
1479
|
+
*/
|
|
1480
|
+
async listDatabases(appId, options) {
|
|
1481
|
+
return paginateAll(async (cursor) => {
|
|
1482
|
+
const params = new URLSearchParams();
|
|
1483
|
+
if (options?.owner)
|
|
1484
|
+
params.set("owner", options.owner);
|
|
1485
|
+
if (cursor)
|
|
1486
|
+
params.set("cursor", cursor);
|
|
1487
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
1488
|
+
const resp = await this.get(`/app/${appId}/api/databases${qs}`);
|
|
1489
|
+
// Tolerant read: a server older than #1958 answers with a bare array.
|
|
1490
|
+
// Without this a published CLI pointed at a not-yet-upgraded server
|
|
1491
|
+
// would print nothing at all. Such a server also predates the `owner`
|
|
1492
|
+
// filter (#1965) and ignores the unknown query parameter, so apply the
|
|
1493
|
+
// filter here — the legacy rows carry `createdBy`, so this reproduces
|
|
1494
|
+
// the server's own post-join filter (#2245).
|
|
1495
|
+
if (Array.isArray(resp)) {
|
|
1496
|
+
return {
|
|
1497
|
+
items: options?.owner
|
|
1498
|
+
? resp.filter((db) => db?.createdBy === options.owner)
|
|
1499
|
+
: resp,
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
return { items: resp?.items ?? [], nextCursor: resp?.nextCursor };
|
|
1503
|
+
});
|
|
1195
1504
|
}
|
|
1196
1505
|
async createDatabase(appId, data) {
|
|
1197
1506
|
return this.post(`/app/${appId}/api/databases`, data);
|
|
@@ -1258,6 +1567,14 @@ export class ApiClient {
|
|
|
1258
1567
|
async describeDatabaseModel(appId, databaseId, modelName) {
|
|
1259
1568
|
return this.get(`/app/${appId}/api/databases/${databaseId}/records/describe`, { modelName });
|
|
1260
1569
|
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Query records in a database model. The Durable Object answers with its own
|
|
1572
|
+
* `{ data, hasMore, nextCursor?, prevCursor? }` envelope; that wire shape is
|
|
1573
|
+
* shared with the JS client, the Swift client and web-admin, so the CLI
|
|
1574
|
+
* normalizes it at its own boundary instead (issue #2357) and hands callers
|
|
1575
|
+
* the same `{ items, hasMore, nextCursor? }` envelope as
|
|
1576
|
+
* {@link queryDocumentRecords}.
|
|
1577
|
+
*/
|
|
1261
1578
|
async queryDatabaseRecords(appId, databaseId, modelName, queryOptions) {
|
|
1262
1579
|
const body = { modelName };
|
|
1263
1580
|
if (queryOptions?.filter)
|
|
@@ -1269,7 +1586,55 @@ export class ApiClient {
|
|
|
1269
1586
|
options.uniqueStartKey = queryOptions.cursor;
|
|
1270
1587
|
if (Object.keys(options).length > 0)
|
|
1271
1588
|
body.options = options;
|
|
1272
|
-
|
|
1589
|
+
const raw = await this.post(`/app/${appId}/api/databases/${databaseId}/records/query`, body);
|
|
1590
|
+
return normalizeCliListEnvelope(raw);
|
|
1591
|
+
}
|
|
1592
|
+
async countDatabaseRecords(appId, databaseId, modelName, filter) {
|
|
1593
|
+
const body = { modelName };
|
|
1594
|
+
if (filter)
|
|
1595
|
+
body.filter = filter;
|
|
1596
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/count`, body);
|
|
1597
|
+
}
|
|
1598
|
+
async aggregateDatabaseRecords(appId, databaseId, modelName, options) {
|
|
1599
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/aggregate`, {
|
|
1600
|
+
modelName,
|
|
1601
|
+
options,
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Upsert one record via `records/save`: creates the record when the id is
|
|
1606
|
+
* new, and merges the supplied fields into it when it already exists (the DO
|
|
1607
|
+
* writes with `ON CONFLICT … json_patch`, so fields you leave out survive).
|
|
1608
|
+
* `patchDatabaseRecord` is the same merge but requires the record to exist.
|
|
1609
|
+
*
|
|
1610
|
+
* `id` is optional: when omitted the body carries no `id`, which the
|
|
1611
|
+
* DatabaseDO only accepts alongside an `upsertOn` field — callers that want a
|
|
1612
|
+
* plain create must supply an id themselves (`databases records save` mints a
|
|
1613
|
+
* ULID, matching the CSV import path).
|
|
1614
|
+
*
|
|
1615
|
+
* Returns the endpoint's flat `{ success, id, appliedFields }` — not the
|
|
1616
|
+
* saved record.
|
|
1617
|
+
*/
|
|
1618
|
+
async saveDatabaseRecord(appId, databaseId, modelName, id, data) {
|
|
1619
|
+
const body = { modelName, data };
|
|
1620
|
+
if (id !== undefined)
|
|
1621
|
+
body.id = id;
|
|
1622
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/save`, body);
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Merge the supplied fields into an existing record via `records/patch`.
|
|
1626
|
+
*
|
|
1627
|
+
* Fields absent from `data` are left as they are. Unlike `saveDatabaseRecord`
|
|
1628
|
+
* the record must already exist — the DatabaseDO answers 404 for an unknown
|
|
1629
|
+
* id rather than creating one. Returns the flat
|
|
1630
|
+
* `{ success, id, appliedFields }`.
|
|
1631
|
+
*/
|
|
1632
|
+
async patchDatabaseRecord(appId, databaseId, modelName, id, data) {
|
|
1633
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/patch`, {
|
|
1634
|
+
modelName,
|
|
1635
|
+
id,
|
|
1636
|
+
data,
|
|
1637
|
+
});
|
|
1273
1638
|
}
|
|
1274
1639
|
// ============================================
|
|
1275
1640
|
// DATABASE OPERATIONS (registered operations)
|
|
@@ -1277,6 +1642,15 @@ export class ApiClient {
|
|
|
1277
1642
|
async listDatabaseOperations(appId, databaseId) {
|
|
1278
1643
|
return this.get(`/app/${appId}/api/databases/${databaseId}/operations`);
|
|
1279
1644
|
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Execute a registered operation and, when the result is a query page,
|
|
1647
|
+
* normalize it to the CLI's one list envelope (issue #2440).
|
|
1648
|
+
*
|
|
1649
|
+
* The registered operation decides the response shape, so the normalization
|
|
1650
|
+
* is conditional — see {@link normalizeDatabaseOperationResult}. Everything
|
|
1651
|
+
* else (a `count`, an `aggregate`, a mutation, a `returnField: "all"`
|
|
1652
|
+
* pipeline) is returned exactly as the server sent it.
|
|
1653
|
+
*/
|
|
1280
1654
|
async executeDatabaseOperation(appId, databaseId, operationName, data, token, options) {
|
|
1281
1655
|
const path = `/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/execute`;
|
|
1282
1656
|
const extraHeaders = {};
|
|
@@ -1284,17 +1658,17 @@ export class ApiClient {
|
|
|
1284
1658
|
extraHeaders["X-Timing"] = "true";
|
|
1285
1659
|
}
|
|
1286
1660
|
if (token) {
|
|
1287
|
-
return this.requestWithToken(path, token, {
|
|
1661
|
+
return normalizeDatabaseOperationResult(await this.requestWithToken(path, token, {
|
|
1288
1662
|
method: "POST",
|
|
1289
1663
|
body: JSON.stringify(data || {}),
|
|
1290
1664
|
headers: extraHeaders,
|
|
1291
|
-
});
|
|
1665
|
+
}));
|
|
1292
1666
|
}
|
|
1293
|
-
return this.request(path, {
|
|
1667
|
+
return normalizeDatabaseOperationResult(await this.request(path, {
|
|
1294
1668
|
method: "POST",
|
|
1295
1669
|
body: JSON.stringify(data || {}),
|
|
1296
1670
|
headers: extraHeaders,
|
|
1297
|
-
});
|
|
1671
|
+
}));
|
|
1298
1672
|
}
|
|
1299
1673
|
/**
|
|
1300
1674
|
* Execute a registered batch (bulk) database operation. Posts a chunk of
|
|
@@ -1357,8 +1731,14 @@ export class ApiClient {
|
|
|
1357
1731
|
async getDatabaseTypeConfig(appId, databaseType) {
|
|
1358
1732
|
return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}`);
|
|
1359
1733
|
}
|
|
1360
|
-
async createDatabaseTypeConfig(appId, data
|
|
1361
|
-
|
|
1734
|
+
async createDatabaseTypeConfig(appId, data,
|
|
1735
|
+
// Issue #1336 (codex round-1 P2): `dryRun` runs every create-time
|
|
1736
|
+
// validation (inline CEL rule lint, manifest shape, schema TOML parse)
|
|
1737
|
+
// WITHOUT persisting, so a fresh-type `config push --dry-run` surfaces a
|
|
1738
|
+
// malformed `defaultAccess`/manifest the same way the real POST would.
|
|
1739
|
+
options) {
|
|
1740
|
+
const query = options?.dryRun ? "?dryRun=true" : "";
|
|
1741
|
+
return this.post(`/app/${appId}/api/databases/types${query}`, data);
|
|
1362
1742
|
}
|
|
1363
1743
|
async updateDatabaseTypeConfig(appId, databaseType, data, expectedModifiedAt, options) {
|
|
1364
1744
|
const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
|
|
@@ -1393,7 +1773,7 @@ export class ApiClient {
|
|
|
1393
1773
|
}
|
|
1394
1774
|
async createDatabaseTypeOperation(appId, databaseType, data,
|
|
1395
1775
|
// Issue #813 (1A): when `dryRun` is set the server runs the op-edit gate
|
|
1396
|
-
// without persisting — used by `
|
|
1776
|
+
// without persisting — used by `config push --dry-run` to surface gate
|
|
1397
1777
|
// failures the real push would hit. `schemaOverride` lets the gate run
|
|
1398
1778
|
// against the schema the SAME push is about to land (so an op that depends
|
|
1399
1779
|
// on a new field isn't falsely rejected against the stale stored schema).
|
|
@@ -1413,16 +1793,24 @@ export class ApiClient {
|
|
|
1413
1793
|
if (options?.dryRun && options.defaultAccess !== undefined) {
|
|
1414
1794
|
body = { ...body, defaultAccess: options.defaultAccess };
|
|
1415
1795
|
}
|
|
1796
|
+
if (options?.dryRun && options.metadataManifestOverride !== undefined) {
|
|
1797
|
+
body = { ...body, metadataManifestOverride: options.metadataManifestOverride };
|
|
1798
|
+
}
|
|
1416
1799
|
return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations${query}`, body);
|
|
1417
1800
|
}
|
|
1418
1801
|
async updateDatabaseTypeOperation(appId, databaseType, name, data, expectedModifiedAt,
|
|
1419
1802
|
// Issue #813 (1A): dry-run runs the op-edit gate without persisting.
|
|
1420
1803
|
// `schemaOverride` gates against the schema the same push is landing.
|
|
1804
|
+
// Issue #1336: `metadataManifestOverride` gates the op-access lint against
|
|
1805
|
+
// the manifest the same push is landing (see `createDatabaseTypeOperation`).
|
|
1421
1806
|
options) {
|
|
1422
1807
|
let body = expectedModifiedAt ? { ...data, expectedModifiedAt } : { ...data };
|
|
1423
1808
|
if (options?.dryRun && options.schemaOverride !== undefined) {
|
|
1424
1809
|
body = { ...body, schemaOverride: options.schemaOverride };
|
|
1425
1810
|
}
|
|
1811
|
+
if (options?.dryRun && options.metadataManifestOverride !== undefined) {
|
|
1812
|
+
body = { ...body, metadataManifestOverride: options.metadataManifestOverride };
|
|
1813
|
+
}
|
|
1426
1814
|
const query = options?.dryRun ? "?dryRun=true" : "";
|
|
1427
1815
|
return this.patch(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}${query}`, body);
|
|
1428
1816
|
}
|
|
@@ -1456,8 +1844,12 @@ export class ApiClient {
|
|
|
1456
1844
|
async createDatabaseTypeSubscription(appId, databaseType, data) {
|
|
1457
1845
|
return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions`, data);
|
|
1458
1846
|
}
|
|
1459
|
-
async updateDatabaseTypeSubscription(appId, databaseType, subscriptionKey, data
|
|
1460
|
-
|
|
1847
|
+
async updateDatabaseTypeSubscription(appId, databaseType, subscriptionKey, data,
|
|
1848
|
+
// #2731 B10 — optimistic concurrency, as the sibling operation update has
|
|
1849
|
+
// had. Omitted (by `--force`, and by every caller predating it) means the
|
|
1850
|
+
// write stays unconditional.
|
|
1851
|
+
expectedModifiedAt) {
|
|
1852
|
+
return this.put(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`, expectedModifiedAt ? { ...data, expectedModifiedAt } : data);
|
|
1461
1853
|
}
|
|
1462
1854
|
async deleteDatabaseTypeSubscription(appId, databaseType, subscriptionKey) {
|
|
1463
1855
|
return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`);
|
|
@@ -1466,7 +1858,16 @@ export class ApiClient {
|
|
|
1466
1858
|
// GROUPS
|
|
1467
1859
|
// ============================================
|
|
1468
1860
|
async listGroups(appId, params) {
|
|
1469
|
-
|
|
1861
|
+
const result = await this.get(`/app/${appId}/api/groups`, params);
|
|
1862
|
+
// #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
|
|
1863
|
+
// so the CLI keeps paginating against pre-#1316 servers.
|
|
1864
|
+
const nextCursor = result?.nextCursor ?? result?.cursor;
|
|
1865
|
+
return {
|
|
1866
|
+
items: result?.items ?? [],
|
|
1867
|
+
nextCursor,
|
|
1868
|
+
hasMore: result?.hasMore ?? nextCursor != null,
|
|
1869
|
+
cursor: nextCursor,
|
|
1870
|
+
};
|
|
1470
1871
|
}
|
|
1471
1872
|
async createGroup(appId, data) {
|
|
1472
1873
|
return this.post(`/app/${appId}/api/groups`, data);
|
|
@@ -1485,7 +1886,16 @@ export class ApiClient {
|
|
|
1485
1886
|
// ============================================
|
|
1486
1887
|
async listGroupMembers(appId, groupType, groupId, options) {
|
|
1487
1888
|
const qs = options?.include ? `?include=${options.include}` : "";
|
|
1488
|
-
|
|
1889
|
+
const result = await this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/members${qs}`);
|
|
1890
|
+
// #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
|
|
1891
|
+
// so the CLI keeps paginating against pre-#1316 servers.
|
|
1892
|
+
const nextCursor = result?.nextCursor ?? result?.cursor;
|
|
1893
|
+
return {
|
|
1894
|
+
items: result?.items ?? [],
|
|
1895
|
+
nextCursor,
|
|
1896
|
+
hasMore: result?.hasMore ?? nextCursor != null,
|
|
1897
|
+
cursor: nextCursor,
|
|
1898
|
+
};
|
|
1489
1899
|
}
|
|
1490
1900
|
async addGroupMember(appId, groupType, groupId, data) {
|
|
1491
1901
|
return this.post(`/app/${appId}/api/groups/${groupType}/${groupId}/members`, data);
|
|
@@ -1530,7 +1940,7 @@ export class ApiClient {
|
|
|
1530
1940
|
const result = await this.get(`/app/${appId}/api/collections${q ? `?${q}` : ""}`);
|
|
1531
1941
|
return {
|
|
1532
1942
|
items: result?.items ?? [],
|
|
1533
|
-
nextCursor: result?.cursor ?? null,
|
|
1943
|
+
nextCursor: result?.nextCursor ?? result?.cursor ?? null,
|
|
1534
1944
|
};
|
|
1535
1945
|
}
|
|
1536
1946
|
async listAllCollections(appId, params) {
|
|
@@ -1543,7 +1953,7 @@ export class ApiClient {
|
|
|
1543
1953
|
const result = await this.get(`/app/${appId}/api/admin/collections${q ? `?${q}` : ""}`);
|
|
1544
1954
|
return {
|
|
1545
1955
|
items: result?.items ?? [],
|
|
1546
|
-
nextCursor: result?.cursor ?? null,
|
|
1956
|
+
nextCursor: result?.nextCursor ?? result?.cursor ?? null,
|
|
1547
1957
|
};
|
|
1548
1958
|
}
|
|
1549
1959
|
async createCollection(appId, data) {
|
|
@@ -1571,7 +1981,7 @@ export class ApiClient {
|
|
|
1571
1981
|
const result = await this.get(`/app/${appId}/api/collections/${collectionId}/documents${q ? `?${q}` : ""}`);
|
|
1572
1982
|
return {
|
|
1573
1983
|
items: result?.items ?? [],
|
|
1574
|
-
nextCursor: result?.cursor ?? null,
|
|
1984
|
+
nextCursor: result?.nextCursor ?? result?.cursor ?? null,
|
|
1575
1985
|
};
|
|
1576
1986
|
}
|
|
1577
1987
|
async addCollectionDocument(appId, collectionId, data) {
|
|
@@ -1590,7 +2000,7 @@ export class ApiClient {
|
|
|
1590
2000
|
const result = await this.get(`/app/${appId}/api/documents/${documentId}/collections${q ? `?${q}` : ""}`);
|
|
1591
2001
|
return {
|
|
1592
2002
|
items: result?.items ?? [],
|
|
1593
|
-
nextCursor: result?.cursor ?? null,
|
|
2003
|
+
nextCursor: result?.nextCursor ?? result?.cursor ?? null,
|
|
1594
2004
|
};
|
|
1595
2005
|
}
|
|
1596
2006
|
// ============================================
|
|
@@ -1650,8 +2060,10 @@ export class ApiClient {
|
|
|
1650
2060
|
* path-addressed PUT route). `resourceType` / `category` identify the config;
|
|
1651
2061
|
* the body carries `schema` / `readRule` / `writeRule` / `description`.
|
|
1652
2062
|
*/
|
|
1653
|
-
async upsertMetadataCategoryConfig(appId, resourceType, category, data
|
|
1654
|
-
|
|
2063
|
+
async upsertMetadataCategoryConfig(appId, resourceType, category, data,
|
|
2064
|
+
/** Optimistic-concurrency token; see `upsertEmailTemplate` (#2731 B10). */
|
|
2065
|
+
expectedModifiedAt) {
|
|
2066
|
+
return this.put(`/app/${appId}/api/metadata-categories/${encodeURIComponent(resourceType)}/${encodeURIComponent(category)}`, expectedModifiedAt ? { ...data, expectedModifiedAt } : data);
|
|
1655
2067
|
}
|
|
1656
2068
|
/**
|
|
1657
2069
|
* Delete a metadata category config (issue #1426, admin-gated route shipped
|
|
@@ -1727,6 +2139,112 @@ export class ApiClient {
|
|
|
1727
2139
|
async getDocument(appId, documentId) {
|
|
1728
2140
|
return this.get(`/app/${appId}/api/documents/${documentId}`);
|
|
1729
2141
|
}
|
|
2142
|
+
/**
|
|
2143
|
+
* Delete a document and everything hanging off it (#2756). The server route
|
|
2144
|
+
* runs the same cascade the client SDK's `documents.delete()` triggers —
|
|
2145
|
+
* Yjs state and update history, blob rows and objects, aliases, user/group
|
|
2146
|
+
* permissions, invitations, collection memberships — and returns
|
|
2147
|
+
* `{ success, message }`, which callers pass through under `--json`.
|
|
2148
|
+
*/
|
|
2149
|
+
async deleteDocument(appId, documentId) {
|
|
2150
|
+
return this.delete(`/app/${appId}/api/documents/${documentId}`);
|
|
2151
|
+
}
|
|
2152
|
+
/**
|
|
2153
|
+
* Introspect a document's Yjs schema (model names, per-model fields and
|
|
2154
|
+
* indexes). Backs `documents records models` and `documents records
|
|
2155
|
+
* describe`. Reuses the existing `GET documents/:id/schema` endpoint.
|
|
2156
|
+
*/
|
|
2157
|
+
async getDocumentSchema(appId, documentId) {
|
|
2158
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/schema`);
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Query records in a document model (issue #1964 Phase 3). Reuses the
|
|
2162
|
+
* server-side `GET documents/:id/records/:model` endpoint, which runs
|
|
2163
|
+
* through the caller's document permission (reader+) or the app-admin arm.
|
|
2164
|
+
* Returns the unified `{ items, hasMore, nextCursor? }` envelope. The cursor
|
|
2165
|
+
* is an opaque token — feed `nextCursor` back as `cursor` for the next page.
|
|
2166
|
+
* The server also dual-emits a deprecated `cursor` alias (#1316); the
|
|
2167
|
+
* normalizer drops it so CLI output is already on `nextCursor` when #1982
|
|
2168
|
+
* removes it.
|
|
2169
|
+
*/
|
|
2170
|
+
async queryDocumentRecords(appId, documentId, modelName, queryOptions) {
|
|
2171
|
+
const query = {};
|
|
2172
|
+
if (queryOptions?.filter)
|
|
2173
|
+
query.filter = JSON.stringify(queryOptions.filter);
|
|
2174
|
+
// Forward `limit` whenever the caller supplied one (including an invalid
|
|
2175
|
+
// 0/negative) so the server validates and returns a clean 400 rather than
|
|
2176
|
+
// silently applying the default.
|
|
2177
|
+
if (queryOptions?.limit !== undefined)
|
|
2178
|
+
query.limit = queryOptions.limit;
|
|
2179
|
+
if (queryOptions?.cursor)
|
|
2180
|
+
query.cursor = queryOptions.cursor;
|
|
2181
|
+
const raw = await this.get(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}`, query);
|
|
2182
|
+
return normalizeCliListEnvelope(raw);
|
|
2183
|
+
}
|
|
2184
|
+
/**
|
|
2185
|
+
* Count records in a document model (issue #1964 Phase 3). Returns `{ count }`.
|
|
2186
|
+
*/
|
|
2187
|
+
async countDocumentRecords(appId, documentId, modelName, queryOptions) {
|
|
2188
|
+
const query = {};
|
|
2189
|
+
if (queryOptions?.filter)
|
|
2190
|
+
query.filter = JSON.stringify(queryOptions.filter);
|
|
2191
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/count`, query);
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Aggregate records in a document model (issue #2437) — the documents twin
|
|
2195
|
+
* of {@link aggregateDatabaseRecords}. `POST documents/:id/records/:model/aggregate`
|
|
2196
|
+
* with `{ options: { operations, groupBy?, filter? } }`, answering `{ result }`
|
|
2197
|
+
* in the same shape as the database endpoint.
|
|
2198
|
+
*
|
|
2199
|
+
* `groupBy` takes plain field names only: StringSet facet grouping and
|
|
2200
|
+
* `{ field, contains }` membership grouping are database-only, which is why
|
|
2201
|
+
* this signature is narrower than the databases one.
|
|
2202
|
+
*/
|
|
2203
|
+
async aggregateDocumentRecords(appId, documentId, modelName, options) {
|
|
2204
|
+
return this.post(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/aggregate`, { options });
|
|
2205
|
+
}
|
|
2206
|
+
/**
|
|
2207
|
+
* Fetch platform-vocabulary document statistics (issue #1964 Phase 3):
|
|
2208
|
+
* record/model/blob counts, approximate size, and last-modified timestamp.
|
|
2209
|
+
*/
|
|
2210
|
+
async getDocumentStats(appId, documentId) {
|
|
2211
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/stats`);
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Create or replace a record in a document model (issue #1964 Phase 4).
|
|
2215
|
+
* `POST documents/:id/records/:model` with `{ id, data, options? }` —
|
|
2216
|
+
* server-side write through the document facade (Yjs-first, CRDT-safe,
|
|
2217
|
+
* broadcasts to connected clients). Requires read-write+ on the document
|
|
2218
|
+
* or the app-admin arm. Returns `{ record }`.
|
|
2219
|
+
*/
|
|
2220
|
+
async saveDocumentRecord(appId, documentId, modelName, body) {
|
|
2221
|
+
return this.post(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}`, body);
|
|
2222
|
+
}
|
|
2223
|
+
/**
|
|
2224
|
+
* Merge fields into an existing record (issue #1964 Phase 4).
|
|
2225
|
+
* `PATCH documents/:id/records/:model/:recordId` with `{ data, options? }`.
|
|
2226
|
+
* Returns `{ record }`.
|
|
2227
|
+
*/
|
|
2228
|
+
async patchDocumentRecord(appId, documentId, modelName, recordId, body) {
|
|
2229
|
+
return this.patch(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/${encodeURIComponent(recordId)}`, body);
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* Delete a record from a document model (issue #1964 Phase 4).
|
|
2233
|
+
* `DELETE documents/:id/records/:model/:recordId`. A missing record id is
|
|
2234
|
+
* a silent no-op per the facade contract. Returns `{ deleted: true }`.
|
|
2235
|
+
*/
|
|
2236
|
+
async deleteDocumentRecord(appId, documentId, modelName, recordId) {
|
|
2237
|
+
return this.delete(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/${encodeURIComponent(recordId)}`);
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* Apply an ordered multi-model create/patch/delete blob atomically (issue
|
|
2241
|
+
* #1964 Phase 4, wrapping the #1517 `bulkUpdate` facade). All-or-nothing:
|
|
2242
|
+
* a validation failure commits nothing. Returns the facade's
|
|
2243
|
+
* `{ applied, added, updated, deleted }`.
|
|
2244
|
+
*/
|
|
2245
|
+
async bulkDocumentRecords(appId, documentId, operations) {
|
|
2246
|
+
return this.post(`/app/${appId}/api/documents/${documentId}/records/bulk`, { operations });
|
|
2247
|
+
}
|
|
1730
2248
|
async createDocument(appId, data) {
|
|
1731
2249
|
return this.post(`/app/${appId}/api/documents`, data);
|
|
1732
2250
|
}
|
|
@@ -1736,6 +2254,24 @@ export class ApiClient {
|
|
|
1736
2254
|
async grantDocumentPermission(appId, documentId, permissions) {
|
|
1737
2255
|
return this.put(`/app/${appId}/api/documents/${documentId}/permissions`, { permissions });
|
|
1738
2256
|
}
|
|
2257
|
+
/**
|
|
2258
|
+
* Revoke a user's permission on a document. Mirrors the published client's
|
|
2259
|
+
* `documents.removePermission` wire behavior (`src/client/api/documentsApi.ts`):
|
|
2260
|
+
* a userId targets the path route `DELETE .../permissions/:userId`, while an
|
|
2261
|
+
* email targets the query-param route `DELETE .../permissions?email=...`
|
|
2262
|
+
* (unified email removal, #452). This duplication of the client's wire shape
|
|
2263
|
+
* is deliberate and parity-tested (issue #1964 amendment 8).
|
|
2264
|
+
*/
|
|
2265
|
+
async revokeDocumentPermission(appId, documentId, target) {
|
|
2266
|
+
if (target.email) {
|
|
2267
|
+
const email = encodeURIComponent(target.email);
|
|
2268
|
+
return this.delete(`/app/${appId}/api/documents/${documentId}/permissions?email=${email}`);
|
|
2269
|
+
}
|
|
2270
|
+
if (!target.userId) {
|
|
2271
|
+
throw new Error("revokeDocumentPermission: userId or email is required");
|
|
2272
|
+
}
|
|
2273
|
+
return this.delete(`/app/${appId}/api/documents/${documentId}/permissions/${target.userId}`);
|
|
2274
|
+
}
|
|
1739
2275
|
async getDocumentLinkAccess(appId, documentId) {
|
|
1740
2276
|
return this.get(`/app/${appId}/api/documents/${documentId}/link-access`);
|
|
1741
2277
|
}
|
|
@@ -1801,15 +2337,16 @@ export class ApiClient {
|
|
|
1801
2337
|
mustNotExist,
|
|
1802
2338
|
});
|
|
1803
2339
|
}
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
2340
|
+
/**
|
|
2341
|
+
* Resolve an app user by exact email — the single email → userId lookup in
|
|
2342
|
+
* the CLI (issue #2763). It reads the member-accessible `users/lookup`
|
|
2343
|
+
* endpoint rather than the admin-only `GET /users` list, and returns the
|
|
2344
|
+
* server's `{ exists, user? }` envelope as-is: a 403 or a transport failure
|
|
2345
|
+
* rejects instead of being flattened into "no such user", which is exactly
|
|
2346
|
+
* how the deleted admin-list helper misreported permission problems.
|
|
2347
|
+
*/
|
|
2348
|
+
async lookupUserByEmail(appId, email) {
|
|
2349
|
+
return this.get(`/app/${appId}/api/users/lookup`, { email });
|
|
1813
2350
|
}
|
|
1814
2351
|
async listAdminDocuments(appId, userId) {
|
|
1815
2352
|
const result = await this.get(`/admin/api/apps/${appId}/documents`, { userId });
|
|
@@ -1818,21 +2355,29 @@ export class ApiClient {
|
|
|
1818
2355
|
// ============================================
|
|
1819
2356
|
// DATABASE EXPORT / IMPORT
|
|
1820
2357
|
// ============================================
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
2358
|
+
// `saveDatabaseRecord` lives with the other record verbs under
|
|
2359
|
+
// "DATABASE RECORDS & SCHEMA" above.
|
|
2360
|
+
/**
|
|
2361
|
+
* Apply an ordered batch of record operations via `records/batch`.
|
|
2362
|
+
*
|
|
2363
|
+
* `atomic` (default off, matching the DatabaseDO) makes the whole batch
|
|
2364
|
+
* all-or-nothing: the DO rolls every write back at the first failed
|
|
2365
|
+
* operation and answers with that operation's status. `databases records
|
|
2366
|
+
* bulk` sends `atomic: true` so it matches the documents twin's
|
|
2367
|
+
* all-or-nothing contract (#2437); the CSV/import paths keep the default
|
|
2368
|
+
* partial-success behavior.
|
|
2369
|
+
*/
|
|
2370
|
+
async batchDatabaseRecords(appId, databaseId, operations, options) {
|
|
2371
|
+
const body = { operations };
|
|
2372
|
+
if (options?.atomic)
|
|
2373
|
+
body.atomic = true;
|
|
2374
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/batch`, body);
|
|
1830
2375
|
}
|
|
1831
2376
|
async deleteDatabaseRecord(appId, databaseId, modelName, id) {
|
|
1832
|
-
return this.post(`/app/${appId}/api/databases/${databaseId}/
|
|
2377
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/delete`, { modelName, id });
|
|
1833
2378
|
}
|
|
1834
2379
|
async batchDeleteDatabaseRecords(appId, databaseId, operations) {
|
|
1835
|
-
return this.post(`/app/${appId}/api/databases/${databaseId}/
|
|
2380
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/batch`, { operations });
|
|
1836
2381
|
}
|
|
1837
2382
|
async listDatabaseUniqueConstraints(appId, databaseId) {
|
|
1838
2383
|
const result = await this.get(`/app/${appId}/api/databases/${databaseId}/records/unique-constraints`);
|
|
@@ -1860,7 +2405,16 @@ export class ApiClient {
|
|
|
1860
2405
|
return this.delete(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`);
|
|
1861
2406
|
}
|
|
1862
2407
|
async listBucketBlobs(appId, bucketIdOrKey, params) {
|
|
1863
|
-
|
|
2408
|
+
const result = await this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`, params);
|
|
2409
|
+
// #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
|
|
2410
|
+
// so the CLI keeps paginating against pre-#1316 servers.
|
|
2411
|
+
const nextCursor = result?.nextCursor ?? result?.cursor;
|
|
2412
|
+
return {
|
|
2413
|
+
items: result?.items ?? [],
|
|
2414
|
+
nextCursor,
|
|
2415
|
+
hasMore: result?.hasMore ?? nextCursor != null,
|
|
2416
|
+
cursor: nextCursor,
|
|
2417
|
+
};
|
|
1864
2418
|
}
|
|
1865
2419
|
async uploadBucketBlob(appId, bucketIdOrKey, data, meta) {
|
|
1866
2420
|
const credentials = await this.ensureAuthenticated();
|
|
@@ -1915,6 +2469,12 @@ export class ApiClient {
|
|
|
1915
2469
|
expiresInSeconds: expiresInSeconds || 300,
|
|
1916
2470
|
});
|
|
1917
2471
|
}
|
|
2472
|
+
// Blob metadata (#1966): head a single blob without downloading its bytes.
|
|
2473
|
+
// Mirrors listBucketBlobs/downloadBucketBlob addressing; the server heads the
|
|
2474
|
+
// R2 object and returns the serialized BlobInfo shape.
|
|
2475
|
+
async getBucketBlobMetadata(appId, bucketIdOrKey, blobId) {
|
|
2476
|
+
return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}/metadata`);
|
|
2477
|
+
}
|
|
1918
2478
|
}
|
|
1919
2479
|
// Export a singleton instance
|
|
1920
2480
|
export const apiClient = new ApiClient();
|