primitive-admin 1.0.43 → 1.0.45
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 +43 -0
- package/assets/skill/skills/primitive-platform/SKILL.md +85 -26
- package/dist/bin/primitive.js +6 -0
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/analytics.js +16 -16
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.js +14 -14
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/auth.js +70 -20
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +11 -11
- package/dist/src/commands/blob-buckets.js.map +1 -1
- package/dist/src/commands/catalog.js +17 -17
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/collection-type-configs.js +5 -5
- package/dist/src/commands/collection-type-configs.js.map +1 -1
- package/dist/src/commands/collections.js +6 -6
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/comparisons.js +6 -6
- package/dist/src/commands/comparisons.js.map +1 -1
- package/dist/src/commands/cron-triggers.js +17 -17
- package/dist/src/commands/cron-triggers.js.map +1 -1
- package/dist/src/commands/database-types.js +13 -13
- package/dist/src/commands/database-types.js.map +1 -1
- package/dist/src/commands/databases.js +132 -8
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/email-templates.js +6 -6
- package/dist/src/commands/email-templates.js.map +1 -1
- package/dist/src/commands/env.js +6 -6
- package/dist/src/commands/env.js.map +1 -1
- package/dist/src/commands/group-type-configs.js +6 -6
- package/dist/src/commands/group-type-configs.js.map +1 -1
- package/dist/src/commands/groups.js +7 -7
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/init.js +175 -144
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.js +31 -21
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/prompts.js +17 -16
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.js +8 -8
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/sync.js +803 -275
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.js +9 -9
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.js +44 -3
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/webhooks.js +18 -18
- package/dist/src/commands/webhooks.js.map +1 -1
- package/dist/src/commands/workflows.js +273 -63
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.js +240 -72
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/migration-nag.js +163 -0
- package/dist/src/lib/migration-nag.js.map +1 -0
- package/dist/src/lib/output.js +58 -6
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/refresh-admin-credentials.js +103 -0
- package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
- package/dist/src/lib/template.js +80 -1
- package/dist/src/lib/template.js.map +1 -1
- package/dist/src/lib/toml-database-config.js +384 -0
- package/dist/src/lib/toml-database-config.js.map +1 -0
- package/dist/src/lib/toml-params-validator.js +183 -0
- package/dist/src/lib/toml-params-validator.js.map +1 -0
- package/dist/src/lib/workflow-fragments.js +121 -0
- package/dist/src/lib/workflow-fragments.js.map +1 -0
- package/dist/src/lib/workflow-toml-validator.js +328 -0
- package/dist/src/lib/workflow-toml-validator.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,26 +1,167 @@
|
|
|
1
1
|
import { loadCredentials, saveCredentials, isTokenExpiringSoon, } from "./config.js";
|
|
2
2
|
import { fetchWithTLS } from "./fetch.js";
|
|
3
3
|
import { paginateAll } from "./paginate.js";
|
|
4
|
+
import { RefreshError, refreshAdminCredentials, } from "./refresh-admin-credentials.js";
|
|
4
5
|
export class ApiError extends Error {
|
|
5
6
|
statusCode;
|
|
6
7
|
code;
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Structured `details` payload from `corsErrorResponse`.
|
|
10
|
+
*
|
|
11
|
+
* The server's error envelope can emit `details` as either:
|
|
12
|
+
* - an array of validation issues (e.g. `[{ path, message }, ...]`) —
|
|
13
|
+
* this is what most legacy endpoints produce, and what sync.ts walks
|
|
14
|
+
* via `Array.isArray(err.details)` / `for (const detail of ...)`.
|
|
15
|
+
* - a record of structured offender fields (e.g. `{ refs, operations,
|
|
16
|
+
* opCount, line, column, ... }`) — emitted by the issue #666 schema
|
|
17
|
+
* gate and consumed by the typed exception subclasses below.
|
|
18
|
+
*
|
|
19
|
+
* Callers must narrow before use: `Array.isArray(err.details)` for the
|
|
20
|
+
* legacy shape, otherwise treat as `Record<string, any>`.
|
|
21
|
+
*/
|
|
22
|
+
details;
|
|
23
|
+
constructor(message, statusCode, code, details) {
|
|
8
24
|
super(message);
|
|
9
25
|
this.statusCode = statusCode;
|
|
10
26
|
this.code = code;
|
|
11
27
|
this.name = "ApiError";
|
|
28
|
+
if (details !== undefined) {
|
|
29
|
+
this.details = details;
|
|
30
|
+
}
|
|
12
31
|
}
|
|
13
32
|
}
|
|
14
33
|
export class ConflictError extends ApiError {
|
|
15
34
|
serverModifiedAt;
|
|
16
35
|
expectedModifiedAt;
|
|
17
|
-
constructor(message, serverModifiedAt, expectedModifiedAt) {
|
|
18
|
-
super(message, 409, "CONFLICT");
|
|
36
|
+
constructor(message, serverModifiedAt, expectedModifiedAt, details) {
|
|
37
|
+
super(message, 409, "CONFLICT", details);
|
|
19
38
|
this.serverModifiedAt = serverModifiedAt;
|
|
20
39
|
this.expectedModifiedAt = expectedModifiedAt;
|
|
21
40
|
this.name = "ConflictError";
|
|
22
41
|
}
|
|
23
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Extract a human-readable error message + structured fields from a non-OK
|
|
45
|
+
* HTTP response body. Single source of truth used by every error-handler call
|
|
46
|
+
* site in this file (see issue #684).
|
|
47
|
+
*/
|
|
48
|
+
export function parseErrorResponse(response, text, path) {
|
|
49
|
+
// Empty body → fall back to status code.
|
|
50
|
+
if (!text) {
|
|
51
|
+
return { message: `HTTP ${response.status}` };
|
|
52
|
+
}
|
|
53
|
+
let errorData;
|
|
54
|
+
try {
|
|
55
|
+
errorData = JSON.parse(text);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Non-JSON body. Surface the existing `<!DOCTYPE` special-case (an HTML
|
|
59
|
+
// 404 page from hitting the wrong path) so we don't regress the helpful
|
|
60
|
+
// "API endpoint not found" message at api-client.ts:343.
|
|
61
|
+
if (text.includes("<!DOCTYPE")) {
|
|
62
|
+
const where = path ? `: ${path}` : "";
|
|
63
|
+
return {
|
|
64
|
+
message: `API endpoint not found${where}. Make sure the server is running.`,
|
|
65
|
+
htmlNotFound: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// Other non-JSON bodies (e.g. plain-text 502 from a proxy) — surface the
|
|
69
|
+
// raw text so the operator at least sees what the server returned.
|
|
70
|
+
return { message: text };
|
|
71
|
+
}
|
|
72
|
+
// Server's standard envelope uses `error`; ConflictError + integrations
|
|
73
|
+
// proxy use `message`. Prefer `error` (more common), fall back to `message`.
|
|
74
|
+
const message = (typeof errorData?.error === "string" && errorData.error) ||
|
|
75
|
+
(typeof errorData?.message === "string" && errorData.message) ||
|
|
76
|
+
`HTTP ${response.status}`;
|
|
77
|
+
// Per issue #666 addendum A1, `code` may be at the top level or nested
|
|
78
|
+
// under `details.code` when the server's bespoke envelope didn't flatten.
|
|
79
|
+
const code = (typeof errorData?.code === "string" ? errorData.code : undefined) ??
|
|
80
|
+
(typeof errorData?.details?.code === "string"
|
|
81
|
+
? errorData.details.code
|
|
82
|
+
: undefined);
|
|
83
|
+
// Accept either an array (legacy) or a plain object (#666 schema gate).
|
|
84
|
+
const details = Array.isArray(errorData?.details)
|
|
85
|
+
? errorData.details
|
|
86
|
+
: errorData?.details && typeof errorData.details === "object"
|
|
87
|
+
? errorData.details
|
|
88
|
+
: undefined;
|
|
89
|
+
return { message, code, details, raw: errorData };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Typed exception classes for the database-schema feature (issue #666).
|
|
93
|
+
* Each maps 1:1 to a server `code` value emitted from the op-edit or
|
|
94
|
+
* schema-edit gate. They all extend ApiError so existing catch-all paths
|
|
95
|
+
* continue to work; specialized catch blocks can branch on `instanceof`.
|
|
96
|
+
*
|
|
97
|
+
* Per round-2 addendum A1, `details` is always preserved so callers can
|
|
98
|
+
* extract structured offender lists (refs[], operations[], etc.).
|
|
99
|
+
*/
|
|
100
|
+
export class SchemaRequiredError extends ApiError {
|
|
101
|
+
constructor(message, details) {
|
|
102
|
+
super(message, 422, "SCHEMA_REQUIRED", details);
|
|
103
|
+
this.name = "SchemaRequiredError";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function detailsRecord(details) {
|
|
107
|
+
return details && !Array.isArray(details) && typeof details === "object"
|
|
108
|
+
? details
|
|
109
|
+
: undefined;
|
|
110
|
+
}
|
|
111
|
+
export class OperationRefError extends ApiError {
|
|
112
|
+
constructor(message, details) {
|
|
113
|
+
super(message, 422, "OPERATION_REFERENCES_UNDEFINED", details);
|
|
114
|
+
this.name = "OperationRefError";
|
|
115
|
+
}
|
|
116
|
+
get refs() {
|
|
117
|
+
const d = detailsRecord(this.details);
|
|
118
|
+
return Array.isArray(d?.refs) ? d.refs : [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export class SchemaBreaksOpsError extends ApiError {
|
|
122
|
+
constructor(message, details) {
|
|
123
|
+
super(message, 422, "SCHEMA_BREAKS_OPERATIONS", details);
|
|
124
|
+
this.name = "SchemaBreaksOpsError";
|
|
125
|
+
}
|
|
126
|
+
get operations() {
|
|
127
|
+
const d = detailsRecord(this.details);
|
|
128
|
+
return Array.isArray(d?.operations) ? d.operations : [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export class SchemaHasUncheckableOpsError extends ApiError {
|
|
132
|
+
constructor(message, details) {
|
|
133
|
+
super(message, 422, "SCHEMA_HAS_UNCHECKABLE_OPS", details);
|
|
134
|
+
this.name = "SchemaHasUncheckableOpsError";
|
|
135
|
+
}
|
|
136
|
+
get operations() {
|
|
137
|
+
const d = detailsRecord(this.details);
|
|
138
|
+
return Array.isArray(d?.operations) ? d.operations : [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export class TomlParseError extends ApiError {
|
|
142
|
+
constructor(message, details) {
|
|
143
|
+
super(message, 400, "TOML_PARSE_ERROR", details);
|
|
144
|
+
this.name = "TomlParseError";
|
|
145
|
+
}
|
|
146
|
+
get line() {
|
|
147
|
+
const d = detailsRecord(this.details);
|
|
148
|
+
return typeof d?.line === "number" ? d.line : undefined;
|
|
149
|
+
}
|
|
150
|
+
get column() {
|
|
151
|
+
const d = detailsRecord(this.details);
|
|
152
|
+
return typeof d?.column === "number" ? d.column : undefined;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export class OpsExistError extends ApiError {
|
|
156
|
+
constructor(message, details) {
|
|
157
|
+
super(message, 409, "OPS_EXIST", details);
|
|
158
|
+
this.name = "OpsExistError";
|
|
159
|
+
}
|
|
160
|
+
get opCount() {
|
|
161
|
+
const d = detailsRecord(this.details);
|
|
162
|
+
return typeof d?.opCount === "number" ? d.opCount : 0;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
24
165
|
export class ApiClient {
|
|
25
166
|
credentials = null;
|
|
26
167
|
constructor() {
|
|
@@ -40,39 +181,24 @@ export class ApiClient {
|
|
|
40
181
|
return this.credentials;
|
|
41
182
|
}
|
|
42
183
|
async refreshToken() {
|
|
43
|
-
if (!this.credentials
|
|
44
|
-
throw new ApiError("
|
|
184
|
+
if (!this.credentials) {
|
|
185
|
+
throw new ApiError("Not logged in. Run 'primitive login' first.", 401);
|
|
45
186
|
}
|
|
46
|
-
const url = `${this.credentials.serverUrl}/admin/api/auth/refresh`;
|
|
47
187
|
try {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
};
|
|
51
|
-
if (this.credentials.globalAdminAppId) {
|
|
52
|
-
headers["X-Global-Admin-App-Id"] = this.credentials.globalAdminAppId;
|
|
53
|
-
}
|
|
54
|
-
const response = await fetchWithTLS(url, {
|
|
55
|
-
method: "POST",
|
|
56
|
-
headers,
|
|
57
|
-
body: JSON.stringify({ refreshToken: this.credentials.refreshToken }),
|
|
58
|
-
});
|
|
59
|
-
if (!response.ok) {
|
|
60
|
-
throw new ApiError("Token refresh failed. Please login again.", 401);
|
|
61
|
-
}
|
|
62
|
-
const data = await response.json();
|
|
63
|
-
// Update credentials with new tokens
|
|
64
|
-
this.credentials = {
|
|
65
|
-
...this.credentials,
|
|
66
|
-
accessToken: data.accessToken || data.token,
|
|
67
|
-
refreshToken: data.refreshToken || this.credentials.refreshToken,
|
|
68
|
-
expiresAt: data.expiresAt,
|
|
69
|
-
};
|
|
188
|
+
const updated = await refreshAdminCredentials(this.credentials);
|
|
189
|
+
this.credentials = updated;
|
|
70
190
|
saveCredentials(this.credentials);
|
|
71
191
|
}
|
|
72
|
-
catch (
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
192
|
+
catch (err) {
|
|
193
|
+
if (err instanceof RefreshError) {
|
|
194
|
+
// Preserve historical behavior: ApiClient surfaces refresh failures
|
|
195
|
+
// as 401s regardless of whether the underlying cause was a network
|
|
196
|
+
// error or a server-side rejection. Callers that need a finer
|
|
197
|
+
// distinction (e.g. `primitive token`) consume RefreshError directly
|
|
198
|
+
// from refresh-admin-credentials.ts instead of going through here.
|
|
199
|
+
throw new ApiError("Token refresh failed. Please login again.", 401);
|
|
200
|
+
}
|
|
201
|
+
throw err;
|
|
76
202
|
}
|
|
77
203
|
}
|
|
78
204
|
async request(path, options = {}) {
|
|
@@ -93,20 +219,42 @@ export class ApiClient {
|
|
|
93
219
|
});
|
|
94
220
|
const text = await response.text();
|
|
95
221
|
if (!response.ok) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
222
|
+
const parsed = parseErrorResponse(response, text, path);
|
|
223
|
+
// Preserve the `<!DOCTYPE` → 404 ApiError shape (status forced to 404).
|
|
224
|
+
if (parsed.htmlNotFound) {
|
|
225
|
+
throw new ApiError(parsed.message, 404);
|
|
99
226
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
227
|
+
// Narrow details to the record shape for the issue #666 typed-exception
|
|
228
|
+
// dispatch below. Conflict metadata may live under `details.*` (canonical
|
|
229
|
+
// location per A1) or on the top-level envelope (legacy).
|
|
230
|
+
const detailsRecord = parsed.details && !Array.isArray(parsed.details)
|
|
231
|
+
? parsed.details
|
|
232
|
+
: undefined;
|
|
233
|
+
const serverModifiedAt = detailsRecord?.serverModifiedAt ?? parsed.raw?.serverModifiedAt;
|
|
234
|
+
const expectedModifiedAt = detailsRecord?.expectedModifiedAt ?? parsed.raw?.expectedModifiedAt;
|
|
235
|
+
// Typed exceptions for the schema-feature (issue #666).
|
|
236
|
+
if (response.status === 409 && parsed.code === "CONFLICT") {
|
|
237
|
+
throw new ConflictError(parsed.message, serverModifiedAt, expectedModifiedAt, detailsRecord);
|
|
105
238
|
}
|
|
106
|
-
if (response.status === 409 &&
|
|
107
|
-
throw new
|
|
239
|
+
if (response.status === 409 && parsed.code === "OPS_EXIST") {
|
|
240
|
+
throw new OpsExistError(parsed.message, detailsRecord);
|
|
108
241
|
}
|
|
109
|
-
|
|
242
|
+
if (response.status === 400 && parsed.code === "TOML_PARSE_ERROR") {
|
|
243
|
+
throw new TomlParseError(parsed.message, detailsRecord);
|
|
244
|
+
}
|
|
245
|
+
if (response.status === 422 && parsed.code === "SCHEMA_REQUIRED") {
|
|
246
|
+
throw new SchemaRequiredError(parsed.message, detailsRecord);
|
|
247
|
+
}
|
|
248
|
+
if (response.status === 422 && parsed.code === "OPERATION_REFERENCES_UNDEFINED") {
|
|
249
|
+
throw new OperationRefError(parsed.message, detailsRecord);
|
|
250
|
+
}
|
|
251
|
+
if (response.status === 422 && parsed.code === "SCHEMA_BREAKS_OPERATIONS") {
|
|
252
|
+
throw new SchemaBreaksOpsError(parsed.message, detailsRecord);
|
|
253
|
+
}
|
|
254
|
+
if (response.status === 422 && parsed.code === "SCHEMA_HAS_UNCHECKABLE_OPS") {
|
|
255
|
+
throw new SchemaHasUncheckableOpsError(parsed.message, detailsRecord);
|
|
256
|
+
}
|
|
257
|
+
throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
|
|
110
258
|
}
|
|
111
259
|
return text ? JSON.parse(text) : null;
|
|
112
260
|
}
|
|
@@ -196,9 +344,29 @@ export class ApiClient {
|
|
|
196
344
|
async mintTestJwt(appId, userId, role) {
|
|
197
345
|
return this.post(`/admin/api/apps/${appId}/users/${userId}/mint-test-jwt`, role ? { role } : {});
|
|
198
346
|
}
|
|
199
|
-
async
|
|
200
|
-
|
|
201
|
-
|
|
347
|
+
async rebuildUserSearchText(appId) {
|
|
348
|
+
return this.post(`/admin/api/apps/${appId}/users/rebuild-search-text`, {});
|
|
349
|
+
}
|
|
350
|
+
async listUsers(appId, params) {
|
|
351
|
+
const query = new URLSearchParams();
|
|
352
|
+
if (params?.name)
|
|
353
|
+
query.set("name", params.name);
|
|
354
|
+
if (params?.email)
|
|
355
|
+
query.set("email", params.email);
|
|
356
|
+
if (params?.userId)
|
|
357
|
+
query.set("userId", params.userId);
|
|
358
|
+
if (params?.limit)
|
|
359
|
+
query.set("limit", String(params.limit));
|
|
360
|
+
if (params?.cursor)
|
|
361
|
+
query.set("cursor", params.cursor);
|
|
362
|
+
const path = query.toString()
|
|
363
|
+
? `/app/${appId}/api/users?${query.toString()}`
|
|
364
|
+
: `/app/${appId}/api/users`;
|
|
365
|
+
const result = await this.get(path);
|
|
366
|
+
return {
|
|
367
|
+
items: result?.items ?? (Array.isArray(result) ? result : []),
|
|
368
|
+
nextCursor: result?.nextCursor ?? null,
|
|
369
|
+
};
|
|
202
370
|
}
|
|
203
371
|
async removeUser(appId, userId) {
|
|
204
372
|
return this.delete(`/app/${appId}/api/users/${userId}`);
|
|
@@ -305,6 +473,10 @@ export class ApiClient {
|
|
|
305
473
|
const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/logs`, params);
|
|
306
474
|
return result?.items ?? [];
|
|
307
475
|
}
|
|
476
|
+
async listWorkflowRunIntegrationLogs(appId, runId, params) {
|
|
477
|
+
const result = await this.get(`/admin/api/apps/${appId}/workflows/runs/${runId}/integration-logs`, params);
|
|
478
|
+
return result?.items ?? [];
|
|
479
|
+
}
|
|
308
480
|
async listIntegrationSecrets(appId, integrationId, params) {
|
|
309
481
|
const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets`, params);
|
|
310
482
|
return result?.items ?? [];
|
|
@@ -540,14 +712,8 @@ export class ApiClient {
|
|
|
540
712
|
});
|
|
541
713
|
const text = await response.text();
|
|
542
714
|
if (!response.ok) {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
errorData = JSON.parse(text);
|
|
546
|
-
}
|
|
547
|
-
catch {
|
|
548
|
-
errorData = { message: text || `HTTP ${response.status}` };
|
|
549
|
-
}
|
|
550
|
-
throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
|
|
715
|
+
const parsed = parseErrorResponse(response, text, url);
|
|
716
|
+
throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
|
|
551
717
|
}
|
|
552
718
|
return text ? JSON.parse(text) : null;
|
|
553
719
|
}
|
|
@@ -567,14 +733,8 @@ export class ApiClient {
|
|
|
567
733
|
});
|
|
568
734
|
if (!response.ok) {
|
|
569
735
|
const text = await response.text();
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
errorData = JSON.parse(text);
|
|
573
|
-
}
|
|
574
|
-
catch {
|
|
575
|
-
errorData = { message: text || `HTTP ${response.status}` };
|
|
576
|
-
}
|
|
577
|
-
throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
|
|
736
|
+
const parsed = parseErrorResponse(response, text, url);
|
|
737
|
+
throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
|
|
578
738
|
}
|
|
579
739
|
const arrayBuffer = await response.arrayBuffer();
|
|
580
740
|
return {
|
|
@@ -995,14 +1155,8 @@ export class ApiClient {
|
|
|
995
1155
|
const response = await fetchWithTLS(url, { ...options, headers });
|
|
996
1156
|
const text = await response.text();
|
|
997
1157
|
if (!response.ok) {
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
errorData = JSON.parse(text);
|
|
1001
|
-
}
|
|
1002
|
-
catch {
|
|
1003
|
-
errorData = { message: text || `HTTP ${response.status}` };
|
|
1004
|
-
}
|
|
1005
|
-
throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
|
|
1158
|
+
const parsed = parseErrorResponse(response, text, path);
|
|
1159
|
+
throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
|
|
1006
1160
|
}
|
|
1007
1161
|
return text ? JSON.parse(text) : null;
|
|
1008
1162
|
}
|
|
@@ -1030,13 +1184,27 @@ export class ApiClient {
|
|
|
1030
1184
|
async createDatabaseTypeConfig(appId, data) {
|
|
1031
1185
|
return this.post(`/app/${appId}/api/databases/types`, data);
|
|
1032
1186
|
}
|
|
1033
|
-
async updateDatabaseTypeConfig(appId, databaseType, data, expectedModifiedAt) {
|
|
1187
|
+
async updateDatabaseTypeConfig(appId, databaseType, data, expectedModifiedAt, options) {
|
|
1034
1188
|
const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
|
|
1035
|
-
|
|
1189
|
+
const qs = new URLSearchParams();
|
|
1190
|
+
if (options?.dryRun)
|
|
1191
|
+
qs.set("dryRun", "true");
|
|
1192
|
+
if (options?.acceptWarnings)
|
|
1193
|
+
qs.set("acceptWarnings", "true");
|
|
1194
|
+
const query = qs.toString();
|
|
1195
|
+
const path = `/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}${query ? `?${query}` : ""}`;
|
|
1196
|
+
return this.patch(path, body);
|
|
1036
1197
|
}
|
|
1037
1198
|
async deleteDatabaseTypeConfig(appId, databaseType) {
|
|
1038
1199
|
return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}`);
|
|
1039
1200
|
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Issue #666 Phase 3: ask the server to scaffold a TOML schema from
|
|
1203
|
+
* existing ops + DO field introspection. Read-only — does NOT persist.
|
|
1204
|
+
*/
|
|
1205
|
+
async scaffoldDatabaseTypeSchema(appId, databaseType) {
|
|
1206
|
+
return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/schema:scaffold`, {});
|
|
1207
|
+
}
|
|
1040
1208
|
// ============================================
|
|
1041
1209
|
// DATABASE TYPE OPERATIONS
|
|
1042
1210
|
// ============================================
|