primitive-admin 1.1.0-alpha.3 → 1.1.0-alpha.31
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 +168 -21
- package/assets/skill/skills/primitive-platform/SKILL.md +226 -0
- package/dist/bin/primitive.js +198 -15
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.js +107 -0
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.js +464 -55
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.js +27 -13
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/auth.js +165 -5
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +354 -0
- package/dist/src/commands/blob-buckets.js.map +1 -0
- 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 +178 -0
- package/dist/src/commands/collection-type-configs.js.map +1 -0
- package/dist/src/commands/collections.js +526 -0
- package/dist/src/commands/collections.js.map +1 -0
- package/dist/src/commands/comparisons.js +6 -6
- package/dist/src/commands/comparisons.js.map +1 -1
- package/dist/src/commands/cron-triggers.js +364 -0
- package/dist/src/commands/cron-triggers.js.map +1 -0
- package/dist/src/commands/database-types.js +462 -0
- package/dist/src/commands/database-types.js.map +1 -0
- package/dist/src/commands/databases.js +1067 -58
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.js +510 -2
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.js +281 -0
- package/dist/src/commands/email-templates.js.map +1 -0
- package/dist/src/commands/env.js +260 -0
- package/dist/src/commands/env.js.map +1 -0
- package/dist/src/commands/group-type-configs.js +189 -0
- package/dist/src/commands/group-type-configs.js.map +1 -0
- package/dist/src/commands/groups.js +28 -25
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/init.js +719 -188
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.js +504 -33
- 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 +366 -0
- package/dist/src/commands/rule-sets.js.map +1 -0
- package/dist/src/commands/secrets.js +108 -0
- package/dist/src/commands/secrets.js.map +1 -0
- package/dist/src/commands/skill.js +29 -0
- package/dist/src/commands/skill.js.map +1 -0
- package/dist/src/commands/sync.js +2363 -182
- 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 +426 -4
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/webhooks.js +386 -0
- package/dist/src/commands/webhooks.js.map +1 -0
- package/dist/src/commands/workflows.js +614 -64
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.js +942 -83
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/config.js +51 -53
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/constants.js +3 -0
- package/dist/src/lib/constants.js.map +1 -0
- package/dist/src/lib/credentials-store.js +307 -0
- package/dist/src/lib/credentials-store.js.map +1 -0
- package/dist/src/lib/env-resolver.js +121 -0
- package/dist/src/lib/env-resolver.js.map +1 -0
- package/dist/src/lib/init-config.js +87 -0
- package/dist/src/lib/init-config.js.map +1 -0
- 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/paginate.js +42 -0
- package/dist/src/lib/paginate.js.map +1 -0
- package/dist/src/lib/project-config.js +209 -0
- package/dist/src/lib/project-config.js.map +1 -0
- 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/skill-installer.js +135 -0
- package/dist/src/lib/skill-installer.js.map +1 -0
- package/dist/src/lib/sync-paths.js +102 -0
- package/dist/src/lib/sync-paths.js.map +1 -0
- package/dist/src/lib/template.js +279 -18
- 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/version-check.js +172 -0
- package/dist/src/lib/version-check.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 +7 -4
|
@@ -1,25 +1,167 @@
|
|
|
1
1
|
import { loadCredentials, saveCredentials, isTokenExpiringSoon, } from "./config.js";
|
|
2
2
|
import { fetchWithTLS } from "./fetch.js";
|
|
3
|
+
import { paginateAll } from "./paginate.js";
|
|
4
|
+
import { RefreshError, refreshAdminCredentials, } from "./refresh-admin-credentials.js";
|
|
3
5
|
export class ApiError extends Error {
|
|
4
6
|
statusCode;
|
|
5
7
|
code;
|
|
6
|
-
|
|
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) {
|
|
7
24
|
super(message);
|
|
8
25
|
this.statusCode = statusCode;
|
|
9
26
|
this.code = code;
|
|
10
27
|
this.name = "ApiError";
|
|
28
|
+
if (details !== undefined) {
|
|
29
|
+
this.details = details;
|
|
30
|
+
}
|
|
11
31
|
}
|
|
12
32
|
}
|
|
13
33
|
export class ConflictError extends ApiError {
|
|
14
34
|
serverModifiedAt;
|
|
15
35
|
expectedModifiedAt;
|
|
16
|
-
constructor(message, serverModifiedAt, expectedModifiedAt) {
|
|
17
|
-
super(message, 409, "CONFLICT");
|
|
36
|
+
constructor(message, serverModifiedAt, expectedModifiedAt, details) {
|
|
37
|
+
super(message, 409, "CONFLICT", details);
|
|
18
38
|
this.serverModifiedAt = serverModifiedAt;
|
|
19
39
|
this.expectedModifiedAt = expectedModifiedAt;
|
|
20
40
|
this.name = "ConflictError";
|
|
21
41
|
}
|
|
22
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
|
+
}
|
|
23
165
|
export class ApiClient {
|
|
24
166
|
credentials = null;
|
|
25
167
|
constructor() {
|
|
@@ -39,39 +181,24 @@ export class ApiClient {
|
|
|
39
181
|
return this.credentials;
|
|
40
182
|
}
|
|
41
183
|
async refreshToken() {
|
|
42
|
-
if (!this.credentials
|
|
43
|
-
throw new ApiError("
|
|
184
|
+
if (!this.credentials) {
|
|
185
|
+
throw new ApiError("Not logged in. Run 'primitive login' first.", 401);
|
|
44
186
|
}
|
|
45
|
-
const url = `${this.credentials.serverUrl}/admin/api/auth/refresh`;
|
|
46
187
|
try {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
};
|
|
50
|
-
if (this.credentials.globalAdminAppId) {
|
|
51
|
-
headers["X-Global-Admin-App-Id"] = this.credentials.globalAdminAppId;
|
|
52
|
-
}
|
|
53
|
-
const response = await fetchWithTLS(url, {
|
|
54
|
-
method: "POST",
|
|
55
|
-
headers,
|
|
56
|
-
body: JSON.stringify({ refreshToken: this.credentials.refreshToken }),
|
|
57
|
-
});
|
|
58
|
-
if (!response.ok) {
|
|
59
|
-
throw new ApiError("Token refresh failed. Please login again.", 401);
|
|
60
|
-
}
|
|
61
|
-
const data = await response.json();
|
|
62
|
-
// Update credentials with new tokens
|
|
63
|
-
this.credentials = {
|
|
64
|
-
...this.credentials,
|
|
65
|
-
accessToken: data.accessToken || data.token,
|
|
66
|
-
refreshToken: data.refreshToken || this.credentials.refreshToken,
|
|
67
|
-
expiresAt: data.expiresAt,
|
|
68
|
-
};
|
|
188
|
+
const updated = await refreshAdminCredentials(this.credentials);
|
|
189
|
+
this.credentials = updated;
|
|
69
190
|
saveCredentials(this.credentials);
|
|
70
191
|
}
|
|
71
|
-
catch (
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
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;
|
|
75
202
|
}
|
|
76
203
|
}
|
|
77
204
|
async request(path, options = {}) {
|
|
@@ -92,20 +219,42 @@ export class ApiClient {
|
|
|
92
219
|
});
|
|
93
220
|
const text = await response.text();
|
|
94
221
|
if (!response.ok) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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);
|
|
98
226
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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);
|
|
238
|
+
}
|
|
239
|
+
if (response.status === 409 && parsed.code === "OPS_EXIST") {
|
|
240
|
+
throw new OpsExistError(parsed.message, detailsRecord);
|
|
241
|
+
}
|
|
242
|
+
if (response.status === 400 && parsed.code === "TOML_PARSE_ERROR") {
|
|
243
|
+
throw new TomlParseError(parsed.message, detailsRecord);
|
|
104
244
|
}
|
|
105
|
-
if (response.status ===
|
|
106
|
-
throw new
|
|
245
|
+
if (response.status === 422 && parsed.code === "SCHEMA_REQUIRED") {
|
|
246
|
+
throw new SchemaRequiredError(parsed.message, detailsRecord);
|
|
107
247
|
}
|
|
108
|
-
|
|
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);
|
|
109
258
|
}
|
|
110
259
|
return text ? JSON.parse(text) : null;
|
|
111
260
|
}
|
|
@@ -155,7 +304,18 @@ export class ApiClient {
|
|
|
155
304
|
// APPS
|
|
156
305
|
// ============================================
|
|
157
306
|
async listApps() {
|
|
158
|
-
|
|
307
|
+
// The server paginates cursor-style (default pageSize=25, cap=100). Historically
|
|
308
|
+
// this method issued a single un-paginated GET and silently truncated to the
|
|
309
|
+
// first page, which broke `primitive use <app-id>` and `primitive apps list`
|
|
310
|
+
// for admins with more than 25 apps (see #436). Iterate every page.
|
|
311
|
+
const apps = await paginateAll(async (cursor) => {
|
|
312
|
+
const qs = new URLSearchParams({ limit: "100" });
|
|
313
|
+
if (cursor)
|
|
314
|
+
qs.set("cursor", cursor);
|
|
315
|
+
const resp = await this.get(`/admin/api/admins/me/apps?${qs.toString()}`);
|
|
316
|
+
return { items: Array.isArray(resp.apps) ? resp.apps : [], nextCursor: resp.nextCursor };
|
|
317
|
+
});
|
|
318
|
+
return { apps };
|
|
159
319
|
}
|
|
160
320
|
async createApp(data) {
|
|
161
321
|
return this.post("/admin/api/apps", data);
|
|
@@ -181,8 +341,32 @@ export class ApiClient {
|
|
|
181
341
|
async addUserByEmail(appId, data) {
|
|
182
342
|
return this.post(`/admin/api/apps/${appId}/users/add-by-email`, data);
|
|
183
343
|
}
|
|
184
|
-
async
|
|
185
|
-
return this.
|
|
344
|
+
async mintTestJwt(appId, userId, role) {
|
|
345
|
+
return this.post(`/admin/api/apps/${appId}/users/${userId}/mint-test-jwt`, role ? { role } : {});
|
|
346
|
+
}
|
|
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
|
+
};
|
|
186
370
|
}
|
|
187
371
|
async removeUser(appId, userId) {
|
|
188
372
|
return this.delete(`/app/${appId}/api/users/${userId}`);
|
|
@@ -196,14 +380,30 @@ export class ApiClient {
|
|
|
196
380
|
async transferAdminOwnership(appId, adminId) {
|
|
197
381
|
return this.post(`/admin/api/apps/${appId}/admins/transfer-ownership`, { adminId });
|
|
198
382
|
}
|
|
383
|
+
// App-level console admin management
|
|
384
|
+
async listAppAdmins(appId) {
|
|
385
|
+
return this.get(`/admin/api/apps/${appId}/admins`);
|
|
386
|
+
}
|
|
387
|
+
async addAppAdmin(appId, data) {
|
|
388
|
+
return this.post(`/admin/api/apps/${appId}/admins`, data);
|
|
389
|
+
}
|
|
390
|
+
async removeAppAdmin(appId, adminId) {
|
|
391
|
+
return this.delete(`/admin/api/apps/${appId}/admins/${adminId}`);
|
|
392
|
+
}
|
|
393
|
+
async listAppAdminInvitations(appId) {
|
|
394
|
+
return this.get(`/admin/api/apps/${appId}/admin-invitations`);
|
|
395
|
+
}
|
|
396
|
+
async deleteAppAdminInvitation(appId, invitationId) {
|
|
397
|
+
return this.delete(`/admin/api/apps/${appId}/admin-invitations/${invitationId}`);
|
|
398
|
+
}
|
|
199
399
|
async transferDocumentOwnership(appId, documentId, newOwnerId) {
|
|
200
400
|
return this.post(`/app/${appId}/api/documents/${documentId}/permissions/transfer`, { newOwnerId });
|
|
201
401
|
}
|
|
202
402
|
// ============================================
|
|
203
403
|
// INVITATIONS
|
|
204
404
|
// ============================================
|
|
205
|
-
async listInvitations(appId) {
|
|
206
|
-
return this.get(`/app/${appId}/api/invitations
|
|
405
|
+
async listInvitations(appId, params) {
|
|
406
|
+
return this.get(`/app/${appId}/api/invitations`, params);
|
|
207
407
|
}
|
|
208
408
|
async createInvitation(appId, data) {
|
|
209
409
|
return this.post(`/app/${appId}/api/invitations`, data);
|
|
@@ -228,6 +428,19 @@ export class ApiClient {
|
|
|
228
428
|
return this.delete(`/app/${appId}/api/waitlist/${waitlistId}`);
|
|
229
429
|
}
|
|
230
430
|
// ============================================
|
|
431
|
+
// DEFERRED GRANTS
|
|
432
|
+
// ============================================
|
|
433
|
+
async listDeferredGrants(appId, params) {
|
|
434
|
+
const result = await this.get(`/app/${appId}/api/deferred-grants`, params);
|
|
435
|
+
return {
|
|
436
|
+
grants: result?.grants ?? [],
|
|
437
|
+
nextCursor: result?.nextCursor ?? null,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
async revokeDeferredGrant(appId, deferredId, type) {
|
|
441
|
+
return this.delete(`/app/${appId}/api/deferred-grants/${deferredId}?type=${type}`);
|
|
442
|
+
}
|
|
443
|
+
// ============================================
|
|
231
444
|
// INTEGRATIONS
|
|
232
445
|
// ============================================
|
|
233
446
|
async listIntegrations(appId, params) {
|
|
@@ -260,6 +473,10 @@ export class ApiClient {
|
|
|
260
473
|
const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/logs`, params);
|
|
261
474
|
return result?.items ?? [];
|
|
262
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
|
+
}
|
|
263
480
|
async listIntegrationSecrets(appId, integrationId, params) {
|
|
264
481
|
const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/secrets`, params);
|
|
265
482
|
return result?.items ?? [];
|
|
@@ -274,6 +491,86 @@ export class ApiClient {
|
|
|
274
491
|
});
|
|
275
492
|
}
|
|
276
493
|
// ============================================
|
|
494
|
+
// APP SECRETS
|
|
495
|
+
// ============================================
|
|
496
|
+
async listAppSecrets(appId) {
|
|
497
|
+
const result = await this.get(`/admin/api/apps/${appId}/secrets`);
|
|
498
|
+
return result?.items ?? [];
|
|
499
|
+
}
|
|
500
|
+
async createAppSecret(appId, payload) {
|
|
501
|
+
return this.post(`/admin/api/apps/${appId}/secrets`, payload);
|
|
502
|
+
}
|
|
503
|
+
async updateAppSecret(appId, secretId, payload) {
|
|
504
|
+
return this.put(`/admin/api/apps/${appId}/secrets/${secretId}`, payload);
|
|
505
|
+
}
|
|
506
|
+
async upsertAppSecret(appId, key, payload) {
|
|
507
|
+
return this.put(`/admin/api/apps/${appId}/secrets/by-key/${key}`, payload);
|
|
508
|
+
}
|
|
509
|
+
async deleteAppSecret(appId, secretId) {
|
|
510
|
+
return this.delete(`/admin/api/apps/${appId}/secrets/${secretId}`);
|
|
511
|
+
}
|
|
512
|
+
// ============================================
|
|
513
|
+
// WEBHOOKS
|
|
514
|
+
// ============================================
|
|
515
|
+
async listWebhooks(appId, params) {
|
|
516
|
+
const result = await this.get(`/admin/api/apps/${appId}/webhooks`, params);
|
|
517
|
+
return {
|
|
518
|
+
items: result?.items ?? [],
|
|
519
|
+
nextCursor: result?.nextCursor ?? null,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
async getWebhook(appId, webhookId) {
|
|
523
|
+
return this.get(`/admin/api/apps/${appId}/webhooks/${webhookId}`);
|
|
524
|
+
}
|
|
525
|
+
async createWebhook(appId, payload) {
|
|
526
|
+
return this.post(`/admin/api/apps/${appId}/webhooks`, payload);
|
|
527
|
+
}
|
|
528
|
+
async updateWebhook(appId, webhookId, payload, expectedModifiedAt) {
|
|
529
|
+
const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
|
|
530
|
+
return this.patch(`/admin/api/apps/${appId}/webhooks/${webhookId}`, body);
|
|
531
|
+
}
|
|
532
|
+
async deleteWebhook(appId, webhookId) {
|
|
533
|
+
return this.delete(`/admin/api/apps/${appId}/webhooks/${webhookId}`);
|
|
534
|
+
}
|
|
535
|
+
async rotateWebhookSecret(appId, webhookId, payload) {
|
|
536
|
+
return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/rotate-secret`, payload);
|
|
537
|
+
}
|
|
538
|
+
async listWebhookEvents(appId, webhookId, params) {
|
|
539
|
+
const result = await this.get(`/admin/api/apps/${appId}/webhooks/${webhookId}/events`, params);
|
|
540
|
+
return result;
|
|
541
|
+
}
|
|
542
|
+
async testWebhook(appId, webhookId, payload) {
|
|
543
|
+
return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/test`, payload || {});
|
|
544
|
+
}
|
|
545
|
+
// ============================================
|
|
546
|
+
// CRON TRIGGERS
|
|
547
|
+
// ============================================
|
|
548
|
+
async listCronTriggers(appId) {
|
|
549
|
+
const result = await this.get(`/app/${appId}/api/cron-triggers`);
|
|
550
|
+
return { items: result?.items ?? [] };
|
|
551
|
+
}
|
|
552
|
+
async getCronTrigger(appId, triggerId) {
|
|
553
|
+
return this.get(`/app/${appId}/api/cron-triggers/${triggerId}`);
|
|
554
|
+
}
|
|
555
|
+
async createCronTrigger(appId, payload) {
|
|
556
|
+
return this.post(`/app/${appId}/api/cron-triggers`, payload);
|
|
557
|
+
}
|
|
558
|
+
async updateCronTrigger(appId, triggerId, payload) {
|
|
559
|
+
return this.put(`/app/${appId}/api/cron-triggers/${triggerId}`, payload);
|
|
560
|
+
}
|
|
561
|
+
async deleteCronTrigger(appId, triggerId) {
|
|
562
|
+
return this.delete(`/app/${appId}/api/cron-triggers/${triggerId}`);
|
|
563
|
+
}
|
|
564
|
+
async pauseCronTrigger(appId, triggerId) {
|
|
565
|
+
return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/pause`, {});
|
|
566
|
+
}
|
|
567
|
+
async resumeCronTrigger(appId, triggerId) {
|
|
568
|
+
return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/resume`, {});
|
|
569
|
+
}
|
|
570
|
+
async testCronTrigger(appId, triggerId) {
|
|
571
|
+
return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/test`, {});
|
|
572
|
+
}
|
|
573
|
+
// ============================================
|
|
277
574
|
// PROMPTS
|
|
278
575
|
// ============================================
|
|
279
576
|
async listPrompts(appId, params) {
|
|
@@ -415,14 +712,8 @@ export class ApiClient {
|
|
|
415
712
|
});
|
|
416
713
|
const text = await response.text();
|
|
417
714
|
if (!response.ok) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
errorData = JSON.parse(text);
|
|
421
|
-
}
|
|
422
|
-
catch {
|
|
423
|
-
errorData = { message: text || `HTTP ${response.status}` };
|
|
424
|
-
}
|
|
425
|
-
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);
|
|
426
717
|
}
|
|
427
718
|
return text ? JSON.parse(text) : null;
|
|
428
719
|
}
|
|
@@ -442,14 +733,8 @@ export class ApiClient {
|
|
|
442
733
|
});
|
|
443
734
|
if (!response.ok) {
|
|
444
735
|
const text = await response.text();
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
errorData = JSON.parse(text);
|
|
448
|
-
}
|
|
449
|
-
catch {
|
|
450
|
-
errorData = { message: text || `HTTP ${response.status}` };
|
|
451
|
-
}
|
|
452
|
-
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);
|
|
453
738
|
}
|
|
454
739
|
const arrayBuffer = await response.arrayBuffer();
|
|
455
740
|
return {
|
|
@@ -519,6 +804,16 @@ export class ApiClient {
|
|
|
519
804
|
async getWorkflowRunStatus(appId, workflowId, runId) {
|
|
520
805
|
return this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/status`);
|
|
521
806
|
}
|
|
807
|
+
async getWorkflowStepRuns(appId, workflowId, runId) {
|
|
808
|
+
const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/steps`);
|
|
809
|
+
return { items: result?.items ?? [] };
|
|
810
|
+
}
|
|
811
|
+
async getWorkflowAnalytics(appId, params) {
|
|
812
|
+
return this.get(`/app/${appId}/api/analytics/workflows/overview`, params);
|
|
813
|
+
}
|
|
814
|
+
async getTopWorkflows(appId, params) {
|
|
815
|
+
return this.get(`/app/${appId}/api/analytics/workflows/top`, params);
|
|
816
|
+
}
|
|
522
817
|
// ============================================
|
|
523
818
|
// WORKFLOW CONFIGURATIONS
|
|
524
819
|
// ============================================
|
|
@@ -547,21 +842,54 @@ export class ApiClient {
|
|
|
547
842
|
// ============================================
|
|
548
843
|
// ANALYTICS
|
|
549
844
|
// ============================================
|
|
550
|
-
async getAnalyticsOverview(appId, params) {
|
|
551
|
-
return this.get(`/app/${appId}/api/analytics/overview`, params);
|
|
552
|
-
}
|
|
553
845
|
async getAnalyticsTopUsers(appId, params) {
|
|
554
846
|
return this.get(`/app/${appId}/api/analytics/users/top`, params);
|
|
555
847
|
}
|
|
556
|
-
async getAnalyticsUserTimeline(appId, userUlid, params) {
|
|
557
|
-
return this.get(`/app/${appId}/api/analytics/users/${userUlid}/timeline`, params);
|
|
558
|
-
}
|
|
559
|
-
async getAnalyticsUserEvents(appId, userUlid, params) {
|
|
560
|
-
return this.get(`/app/${appId}/api/analytics/users/${userUlid}/events`, params);
|
|
561
|
-
}
|
|
562
848
|
async getAnalyticsIntegrationMetrics(appId, params) {
|
|
563
849
|
return this.get(`/app/${appId}/api/analytics/integrations`, params);
|
|
564
850
|
}
|
|
851
|
+
async getAnalyticsOverviewDau(appId) {
|
|
852
|
+
return this.get(`/app/${appId}/api/analytics/overview/dau`);
|
|
853
|
+
}
|
|
854
|
+
async getAnalyticsOverviewWau(appId) {
|
|
855
|
+
return this.get(`/app/${appId}/api/analytics/overview/wau`);
|
|
856
|
+
}
|
|
857
|
+
async getAnalyticsOverviewMau(appId) {
|
|
858
|
+
return this.get(`/app/${appId}/api/analytics/overview/mau`);
|
|
859
|
+
}
|
|
860
|
+
async getAnalyticsOverviewGrowth(appId, params) {
|
|
861
|
+
return this.get(`/app/${appId}/api/analytics/overview/growth`, params);
|
|
862
|
+
}
|
|
863
|
+
async getAnalyticsDailyActive(appId, params) {
|
|
864
|
+
return this.get(`/app/${appId}/api/analytics/daily-active`, params);
|
|
865
|
+
}
|
|
866
|
+
async getAnalyticsRollingActive(appId, params) {
|
|
867
|
+
return this.get(`/app/${appId}/api/analytics/rolling-active`, params);
|
|
868
|
+
}
|
|
869
|
+
async getAnalyticsCohortRetention(appId) {
|
|
870
|
+
return this.get(`/app/${appId}/api/analytics/cohort-retention`);
|
|
871
|
+
}
|
|
872
|
+
async getAnalyticsUserSearch(appId, params) {
|
|
873
|
+
return this.get(`/app/${appId}/api/analytics/users/search`, params);
|
|
874
|
+
}
|
|
875
|
+
async getAnalyticsUserDetail(appId, userUlid) {
|
|
876
|
+
return this.get(`/app/${appId}/api/analytics/users/${userUlid}/detail`);
|
|
877
|
+
}
|
|
878
|
+
async getAnalyticsUserSnapshot(appId, userUlid) {
|
|
879
|
+
return this.get(`/app/${appId}/api/analytics/users/${userUlid}/snapshot`);
|
|
880
|
+
}
|
|
881
|
+
async getAnalyticsEvents(appId, params) {
|
|
882
|
+
return this.get(`/app/${appId}/api/analytics/events`, params);
|
|
883
|
+
}
|
|
884
|
+
async getAnalyticsEventsGrouped(appId, params) {
|
|
885
|
+
return this.get(`/app/${appId}/api/analytics/events/grouped`, params);
|
|
886
|
+
}
|
|
887
|
+
async getAnalyticsTopWorkflows(appId, params) {
|
|
888
|
+
return this.get(`/app/${appId}/api/analytics/workflows/top`, params);
|
|
889
|
+
}
|
|
890
|
+
async getAnalyticsTopPrompts(appId, params) {
|
|
891
|
+
return this.get(`/app/${appId}/api/analytics/prompts/top`, params);
|
|
892
|
+
}
|
|
565
893
|
// ============================================
|
|
566
894
|
// SUPER ADMIN: ADMINS
|
|
567
895
|
// ============================================
|
|
@@ -670,6 +998,24 @@ export class ApiClient {
|
|
|
670
998
|
return this.get(`/admin/api/apps/${appId}/comparisons/${group}`);
|
|
671
999
|
}
|
|
672
1000
|
// ============================================
|
|
1001
|
+
// EMAIL TEMPLATES
|
|
1002
|
+
// ============================================
|
|
1003
|
+
async listEmailTemplates(appId) {
|
|
1004
|
+
return this.get(`/admin/api/apps/${appId}/email-templates`);
|
|
1005
|
+
}
|
|
1006
|
+
async getEmailTemplate(appId, emailType) {
|
|
1007
|
+
return this.get(`/admin/api/apps/${appId}/email-templates/${emailType}`);
|
|
1008
|
+
}
|
|
1009
|
+
async upsertEmailTemplate(appId, emailType, payload) {
|
|
1010
|
+
return this.put(`/admin/api/apps/${appId}/email-templates/${emailType}`, payload);
|
|
1011
|
+
}
|
|
1012
|
+
async deleteEmailTemplate(appId, emailType) {
|
|
1013
|
+
return this.delete(`/admin/api/apps/${appId}/email-templates/${emailType}`);
|
|
1014
|
+
}
|
|
1015
|
+
async testEmailTemplate(appId, emailType, payload) {
|
|
1016
|
+
return this.post(`/admin/api/apps/${appId}/email-templates/${emailType}/test`, payload || {});
|
|
1017
|
+
}
|
|
1018
|
+
// ============================================
|
|
673
1019
|
// ACCESS TOKENS
|
|
674
1020
|
// ============================================
|
|
675
1021
|
async createToken(appId, data) {
|
|
@@ -697,32 +1043,186 @@ export class ApiClient {
|
|
|
697
1043
|
async getDatabase(appId, databaseId) {
|
|
698
1044
|
return this.get(`/app/${appId}/api/databases/${databaseId}`);
|
|
699
1045
|
}
|
|
1046
|
+
async updateDatabase(appId, databaseId, data) {
|
|
1047
|
+
return this.patch(`/app/${appId}/api/databases/${databaseId}`, data);
|
|
1048
|
+
}
|
|
700
1049
|
async deleteDatabase(appId, databaseId) {
|
|
701
1050
|
return this.delete(`/app/${appId}/api/databases/${databaseId}`);
|
|
702
1051
|
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Read a database's CEL context dict.
|
|
1054
|
+
*
|
|
1055
|
+
* The HTTP path stays `/metadata` because the wire field name is still
|
|
1056
|
+
* `metadata`; only the client/CLI-facing helper names were reframed.
|
|
1057
|
+
*/
|
|
1058
|
+
async getDatabaseCelContext(appId, databaseId) {
|
|
1059
|
+
return this.get(`/app/${appId}/api/databases/${databaseId}/metadata`);
|
|
1060
|
+
}
|
|
1061
|
+
/** Update a database's CEL context dict (merge with existing). */
|
|
1062
|
+
async updateDatabaseCelContext(appId, databaseId, celContext) {
|
|
1063
|
+
return this.patch(`/app/${appId}/api/databases/${databaseId}/metadata`, celContext);
|
|
1064
|
+
}
|
|
1065
|
+
/** @deprecated Use {@link getDatabaseCelContext} instead. */
|
|
1066
|
+
async getDatabaseMetadata(appId, databaseId) {
|
|
1067
|
+
return this.getDatabaseCelContext(appId, databaseId);
|
|
1068
|
+
}
|
|
1069
|
+
/** @deprecated Use {@link updateDatabaseCelContext} instead. */
|
|
1070
|
+
async updateDatabaseMetadata(appId, databaseId, metadata) {
|
|
1071
|
+
return this.updateDatabaseCelContext(appId, databaseId, metadata);
|
|
1072
|
+
}
|
|
703
1073
|
// ============================================
|
|
704
1074
|
// DATABASE PERMISSIONS
|
|
705
1075
|
// ============================================
|
|
706
1076
|
async listDatabasePermissions(appId, databaseId) {
|
|
707
1077
|
return this.get(`/app/${appId}/api/databases/${databaseId}/permissions`);
|
|
708
1078
|
}
|
|
1079
|
+
async addDatabaseManager(appId, databaseId, userId) {
|
|
1080
|
+
return this.put(`/app/${appId}/api/databases/${databaseId}/permissions`, {
|
|
1081
|
+
userId,
|
|
1082
|
+
permission: "manager",
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
async removeDatabaseManager(appId, databaseId, userId) {
|
|
1086
|
+
return this.delete(`/app/${appId}/api/databases/${databaseId}/permissions/${userId}`);
|
|
1087
|
+
}
|
|
1088
|
+
/** @deprecated Use {@link addDatabaseManager} instead. */
|
|
709
1089
|
async grantDatabasePermission(appId, databaseId, data) {
|
|
710
|
-
return this.
|
|
1090
|
+
return this.addDatabaseManager(appId, databaseId, data.userId);
|
|
711
1091
|
}
|
|
1092
|
+
/** @deprecated Use {@link removeDatabaseManager} instead. */
|
|
712
1093
|
async revokeDatabasePermission(appId, databaseId, userId) {
|
|
713
1094
|
return this.delete(`/app/${appId}/api/databases/${databaseId}/permissions/${userId}`);
|
|
714
1095
|
}
|
|
715
1096
|
// ============================================
|
|
716
|
-
// DATABASE
|
|
1097
|
+
// DATABASE RECORDS & SCHEMA
|
|
1098
|
+
// ============================================
|
|
1099
|
+
async listDatabaseModels(appId, databaseId) {
|
|
1100
|
+
return this.get(`/app/${appId}/api/databases/${databaseId}/records/models`);
|
|
1101
|
+
}
|
|
1102
|
+
async describeDatabaseModel(appId, databaseId, modelName) {
|
|
1103
|
+
return this.get(`/app/${appId}/api/databases/${databaseId}/records/describe`, { modelName });
|
|
1104
|
+
}
|
|
1105
|
+
async queryDatabaseRecords(appId, databaseId, modelName, queryOptions) {
|
|
1106
|
+
const body = { modelName };
|
|
1107
|
+
if (queryOptions?.filter)
|
|
1108
|
+
body.filter = queryOptions.filter;
|
|
1109
|
+
const options = {};
|
|
1110
|
+
if (queryOptions?.limit)
|
|
1111
|
+
options.limit = queryOptions.limit;
|
|
1112
|
+
if (queryOptions?.cursor)
|
|
1113
|
+
options.uniqueStartKey = queryOptions.cursor;
|
|
1114
|
+
if (Object.keys(options).length > 0)
|
|
1115
|
+
body.options = options;
|
|
1116
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/query`, body);
|
|
1117
|
+
}
|
|
1118
|
+
// ============================================
|
|
1119
|
+
// DATABASE OPERATIONS (registered operations)
|
|
1120
|
+
// ============================================
|
|
1121
|
+
async listDatabaseOperations(appId, databaseId) {
|
|
1122
|
+
return this.get(`/app/${appId}/api/databases/${databaseId}/operations`);
|
|
1123
|
+
}
|
|
1124
|
+
async executeDatabaseOperation(appId, databaseId, operationName, data, token, options) {
|
|
1125
|
+
const path = `/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/execute`;
|
|
1126
|
+
const extraHeaders = {};
|
|
1127
|
+
if (options?.timing) {
|
|
1128
|
+
extraHeaders["X-Timing"] = "true";
|
|
1129
|
+
}
|
|
1130
|
+
if (token) {
|
|
1131
|
+
return this.requestWithToken(path, token, {
|
|
1132
|
+
method: "POST",
|
|
1133
|
+
body: JSON.stringify(data || {}),
|
|
1134
|
+
headers: extraHeaders,
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
return this.request(path, {
|
|
1138
|
+
method: "POST",
|
|
1139
|
+
body: JSON.stringify(data || {}),
|
|
1140
|
+
headers: extraHeaders,
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Make a request using a specific JWT token instead of the CLI's credentials.
|
|
1145
|
+
* Used for executing operations as a different user.
|
|
1146
|
+
*/
|
|
1147
|
+
async requestWithToken(path, token, options = {}) {
|
|
1148
|
+
const credentials = await this.ensureAuthenticated();
|
|
1149
|
+
const url = `${credentials.serverUrl}${path}`;
|
|
1150
|
+
const headers = {
|
|
1151
|
+
Authorization: `Bearer ${token}`,
|
|
1152
|
+
"Content-Type": "application/json",
|
|
1153
|
+
...(options.headers || {}),
|
|
1154
|
+
};
|
|
1155
|
+
const response = await fetchWithTLS(url, { ...options, headers });
|
|
1156
|
+
const text = await response.text();
|
|
1157
|
+
if (!response.ok) {
|
|
1158
|
+
const parsed = parseErrorResponse(response, text, path);
|
|
1159
|
+
throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
|
|
1160
|
+
}
|
|
1161
|
+
return text ? JSON.parse(text) : null;
|
|
1162
|
+
}
|
|
1163
|
+
// ============================================
|
|
1164
|
+
// DATABASE INDEXES
|
|
1165
|
+
// ============================================
|
|
1166
|
+
async listDatabaseIndexes(appId, databaseId, modelName) {
|
|
1167
|
+
return this.get(`/app/${appId}/api/databases/${databaseId}/records/indexes`, modelName ? { modelName } : undefined);
|
|
1168
|
+
}
|
|
1169
|
+
async registerDatabaseIndex(appId, databaseId, data) {
|
|
1170
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/index/register`, data);
|
|
1171
|
+
}
|
|
1172
|
+
async dropDatabaseIndex(appId, databaseId, data) {
|
|
1173
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/index/drop`, data);
|
|
1174
|
+
}
|
|
1175
|
+
// ============================================
|
|
1176
|
+
// DATABASE TYPE CONFIGS
|
|
717
1177
|
// ============================================
|
|
718
|
-
async
|
|
719
|
-
return this.get(`/app/${appId}/api/databases
|
|
1178
|
+
async listDatabaseTypeConfigs(appId) {
|
|
1179
|
+
return this.get(`/app/${appId}/api/databases/types`);
|
|
1180
|
+
}
|
|
1181
|
+
async getDatabaseTypeConfig(appId, databaseType) {
|
|
1182
|
+
return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}`);
|
|
1183
|
+
}
|
|
1184
|
+
async createDatabaseTypeConfig(appId, data) {
|
|
1185
|
+
return this.post(`/app/${appId}/api/databases/types`, data);
|
|
1186
|
+
}
|
|
1187
|
+
async updateDatabaseTypeConfig(appId, databaseType, data, expectedModifiedAt, options) {
|
|
1188
|
+
const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
|
|
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);
|
|
1197
|
+
}
|
|
1198
|
+
async deleteDatabaseTypeConfig(appId, databaseType) {
|
|
1199
|
+
return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}`);
|
|
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`, {});
|
|
720
1207
|
}
|
|
721
|
-
|
|
722
|
-
|
|
1208
|
+
// ============================================
|
|
1209
|
+
// DATABASE TYPE OPERATIONS
|
|
1210
|
+
// ============================================
|
|
1211
|
+
async listDatabaseTypeOperations(appId, databaseType) {
|
|
1212
|
+
return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations`);
|
|
1213
|
+
}
|
|
1214
|
+
async getDatabaseTypeOperation(appId, databaseType, name) {
|
|
1215
|
+
return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`);
|
|
723
1216
|
}
|
|
724
|
-
async
|
|
725
|
-
return this.
|
|
1217
|
+
async createDatabaseTypeOperation(appId, databaseType, data) {
|
|
1218
|
+
return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations`, data);
|
|
1219
|
+
}
|
|
1220
|
+
async updateDatabaseTypeOperation(appId, databaseType, name, data, expectedModifiedAt) {
|
|
1221
|
+
const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
|
|
1222
|
+
return this.patch(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`, body);
|
|
1223
|
+
}
|
|
1224
|
+
async deleteDatabaseTypeOperation(appId, databaseType, name) {
|
|
1225
|
+
return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`);
|
|
726
1226
|
}
|
|
727
1227
|
// ============================================
|
|
728
1228
|
// GROUPS
|
|
@@ -754,8 +1254,11 @@ export class ApiClient {
|
|
|
754
1254
|
async removeGroupMember(appId, groupType, groupId, userId) {
|
|
755
1255
|
return this.delete(`/app/${appId}/api/groups/${groupType}/${groupId}/members/${userId}`);
|
|
756
1256
|
}
|
|
757
|
-
|
|
758
|
-
|
|
1257
|
+
// ============================================
|
|
1258
|
+
// GROUP RESOURCE LISTINGS
|
|
1259
|
+
// ============================================
|
|
1260
|
+
async listGroupDocuments(appId, groupType, groupId) {
|
|
1261
|
+
return this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/documents`);
|
|
759
1262
|
}
|
|
760
1263
|
// ============================================
|
|
761
1264
|
// USER MEMBERSHIPS
|
|
@@ -763,6 +1266,362 @@ export class ApiClient {
|
|
|
763
1266
|
async listUserMemberships(appId, userId) {
|
|
764
1267
|
return this.get(`/app/${appId}/api/users/${userId}/memberships`);
|
|
765
1268
|
}
|
|
1269
|
+
// ============================================
|
|
1270
|
+
// DOCUMENT GROUP PERMISSIONS
|
|
1271
|
+
// ============================================
|
|
1272
|
+
async listDocumentGroupPermissions(appId, documentId) {
|
|
1273
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/group-permissions`);
|
|
1274
|
+
}
|
|
1275
|
+
async grantDocumentGroupPermission(appId, documentId, data) {
|
|
1276
|
+
return this.post(`/app/${appId}/api/documents/${documentId}/group-permissions`, data);
|
|
1277
|
+
}
|
|
1278
|
+
async revokeDocumentGroupPermission(appId, documentId, groupType, groupId) {
|
|
1279
|
+
return this.delete(`/app/${appId}/api/documents/${documentId}/group-permissions/${groupType}/${groupId}`);
|
|
1280
|
+
}
|
|
1281
|
+
// ============================================
|
|
1282
|
+
// COLLECTIONS
|
|
1283
|
+
// ============================================
|
|
1284
|
+
async listCollections(appId, params) {
|
|
1285
|
+
const qs = new URLSearchParams();
|
|
1286
|
+
if (params?.limit)
|
|
1287
|
+
qs.set("limit", String(params.limit));
|
|
1288
|
+
if (params?.cursor)
|
|
1289
|
+
qs.set("cursor", params.cursor);
|
|
1290
|
+
const q = qs.toString();
|
|
1291
|
+
const result = await this.get(`/app/${appId}/api/collections${q ? `?${q}` : ""}`);
|
|
1292
|
+
return {
|
|
1293
|
+
items: result?.items ?? [],
|
|
1294
|
+
nextCursor: result?.cursor ?? null,
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
async listAllCollections(appId, params) {
|
|
1298
|
+
const qs = new URLSearchParams();
|
|
1299
|
+
if (params?.limit)
|
|
1300
|
+
qs.set("limit", String(params.limit));
|
|
1301
|
+
if (params?.cursor)
|
|
1302
|
+
qs.set("cursor", params.cursor);
|
|
1303
|
+
const q = qs.toString();
|
|
1304
|
+
const result = await this.get(`/app/${appId}/api/admin/collections${q ? `?${q}` : ""}`);
|
|
1305
|
+
return {
|
|
1306
|
+
items: result?.items ?? [],
|
|
1307
|
+
nextCursor: result?.cursor ?? null,
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
async createCollection(appId, data) {
|
|
1311
|
+
return this.post(`/app/${appId}/api/collections`, data);
|
|
1312
|
+
}
|
|
1313
|
+
async getCollection(appId, collectionId) {
|
|
1314
|
+
return this.get(`/app/${appId}/api/collections/${collectionId}`);
|
|
1315
|
+
}
|
|
1316
|
+
async updateCollection(appId, collectionId, data) {
|
|
1317
|
+
return this.patch(`/app/${appId}/api/collections/${collectionId}`, data);
|
|
1318
|
+
}
|
|
1319
|
+
async deleteCollection(appId, collectionId) {
|
|
1320
|
+
return this.delete(`/app/${appId}/api/collections/${collectionId}`);
|
|
1321
|
+
}
|
|
1322
|
+
// ============================================
|
|
1323
|
+
// COLLECTION DOCUMENTS
|
|
1324
|
+
// ============================================
|
|
1325
|
+
async listCollectionDocuments(appId, collectionId, params) {
|
|
1326
|
+
const qs = new URLSearchParams();
|
|
1327
|
+
if (params?.limit)
|
|
1328
|
+
qs.set("limit", String(params.limit));
|
|
1329
|
+
if (params?.cursor)
|
|
1330
|
+
qs.set("cursor", params.cursor);
|
|
1331
|
+
const q = qs.toString();
|
|
1332
|
+
const result = await this.get(`/app/${appId}/api/collections/${collectionId}/documents${q ? `?${q}` : ""}`);
|
|
1333
|
+
return {
|
|
1334
|
+
items: result?.items ?? [],
|
|
1335
|
+
nextCursor: result?.cursor ?? null,
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
async addCollectionDocument(appId, collectionId, data) {
|
|
1339
|
+
return this.post(`/app/${appId}/api/collections/${collectionId}/documents`, data);
|
|
1340
|
+
}
|
|
1341
|
+
async removeCollectionDocument(appId, collectionId, documentId) {
|
|
1342
|
+
return this.delete(`/app/${appId}/api/collections/${collectionId}/documents/${documentId}`);
|
|
1343
|
+
}
|
|
1344
|
+
async listCollectionsForDocument(appId, documentId, params) {
|
|
1345
|
+
const qs = new URLSearchParams();
|
|
1346
|
+
if (params?.limit)
|
|
1347
|
+
qs.set("limit", String(params.limit));
|
|
1348
|
+
if (params?.cursor)
|
|
1349
|
+
qs.set("cursor", params.cursor);
|
|
1350
|
+
const q = qs.toString();
|
|
1351
|
+
const result = await this.get(`/app/${appId}/api/documents/${documentId}/collections${q ? `?${q}` : ""}`);
|
|
1352
|
+
return {
|
|
1353
|
+
items: result?.items ?? [],
|
|
1354
|
+
nextCursor: result?.cursor ?? null,
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
// ============================================
|
|
1358
|
+
// COLLECTION ACCESS (GROUPS + MEMBERS)
|
|
1359
|
+
// ============================================
|
|
1360
|
+
async getCollectionAccess(appId, collectionId) {
|
|
1361
|
+
return this.get(`/app/${appId}/api/collections/${collectionId}/access`);
|
|
1362
|
+
}
|
|
1363
|
+
async grantCollectionGroupPermission(appId, collectionId, data) {
|
|
1364
|
+
return this.post(`/app/${appId}/api/collections/${collectionId}/group-permissions`, data);
|
|
1365
|
+
}
|
|
1366
|
+
async revokeCollectionGroupPermission(appId, collectionId, groupType, groupId) {
|
|
1367
|
+
return this.delete(`/app/${appId}/api/collections/${collectionId}/group-permissions/${groupType}/${groupId}`);
|
|
1368
|
+
}
|
|
1369
|
+
async addCollectionMember(appId, collectionId, data) {
|
|
1370
|
+
return this.post(`/app/${appId}/api/collections/${collectionId}/members`, data);
|
|
1371
|
+
}
|
|
1372
|
+
async removeCollectionMember(appId, collectionId, userId) {
|
|
1373
|
+
return this.delete(`/app/${appId}/api/collections/${collectionId}/members/${userId}`);
|
|
1374
|
+
}
|
|
1375
|
+
// ============================================
|
|
1376
|
+
// COLLECTION TYPE CONFIGS
|
|
1377
|
+
// ============================================
|
|
1378
|
+
async listCollectionTypeConfigs(appId) {
|
|
1379
|
+
return this.get(`/app/${appId}/api/collection-type-configs`);
|
|
1380
|
+
}
|
|
1381
|
+
async getCollectionTypeConfig(appId, collectionType) {
|
|
1382
|
+
return this.get(`/app/${appId}/api/collection-type-configs/${collectionType}`);
|
|
1383
|
+
}
|
|
1384
|
+
async createCollectionTypeConfig(appId, data) {
|
|
1385
|
+
return this.post(`/app/${appId}/api/collection-type-configs`, data);
|
|
1386
|
+
}
|
|
1387
|
+
async updateCollectionTypeConfig(appId, collectionType, data, expectedModifiedAt) {
|
|
1388
|
+
const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
|
|
1389
|
+
return this.patch(`/app/${appId}/api/collection-type-configs/${collectionType}`, body);
|
|
1390
|
+
}
|
|
1391
|
+
async deleteCollectionTypeConfig(appId, collectionType) {
|
|
1392
|
+
return this.delete(`/app/${appId}/api/collection-type-configs/${collectionType}`);
|
|
1393
|
+
}
|
|
1394
|
+
// ============================================
|
|
1395
|
+
// ACCESS RULE SETS
|
|
1396
|
+
// ============================================
|
|
1397
|
+
async listRuleSets(appId, params) {
|
|
1398
|
+
return this.get(`/app/${appId}/api/rule-sets`, params);
|
|
1399
|
+
}
|
|
1400
|
+
async createRuleSet(appId, data) {
|
|
1401
|
+
return this.post(`/app/${appId}/api/rule-sets`, data);
|
|
1402
|
+
}
|
|
1403
|
+
async getRuleSet(appId, ruleSetId) {
|
|
1404
|
+
return this.get(`/app/${appId}/api/rule-sets/${ruleSetId}`);
|
|
1405
|
+
}
|
|
1406
|
+
async updateRuleSet(appId, ruleSetId, data, expectedModifiedAt) {
|
|
1407
|
+
const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
|
|
1408
|
+
return this.patch(`/app/${appId}/api/rule-sets/${ruleSetId}`, body);
|
|
1409
|
+
}
|
|
1410
|
+
async deleteRuleSet(appId, ruleSetId) {
|
|
1411
|
+
return this.delete(`/app/${appId}/api/rule-sets/${ruleSetId}`);
|
|
1412
|
+
}
|
|
1413
|
+
async getRuleSetSchema(appId) {
|
|
1414
|
+
return this.get(`/app/${appId}/api/rule-sets/schema`);
|
|
1415
|
+
}
|
|
1416
|
+
async testRuleSet(appId, ruleSetId, data) {
|
|
1417
|
+
return this.post(`/app/${appId}/api/rule-sets/${ruleSetId}/test`, data);
|
|
1418
|
+
}
|
|
1419
|
+
async debugRuleSet(appId, data) {
|
|
1420
|
+
return this.post(`/app/${appId}/api/rule-sets/debug`, data);
|
|
1421
|
+
}
|
|
1422
|
+
// ============================================
|
|
1423
|
+
// GROUP TYPE CONFIGS
|
|
1424
|
+
// ============================================
|
|
1425
|
+
async listGroupTypeConfigs(appId) {
|
|
1426
|
+
return this.get(`/app/${appId}/api/group-type-configs`);
|
|
1427
|
+
}
|
|
1428
|
+
async getGroupTypeConfig(appId, groupType) {
|
|
1429
|
+
return this.get(`/app/${appId}/api/group-type-configs/${groupType}`);
|
|
1430
|
+
}
|
|
1431
|
+
async createGroupTypeConfig(appId, data) {
|
|
1432
|
+
return this.post(`/app/${appId}/api/group-type-configs`, data);
|
|
1433
|
+
}
|
|
1434
|
+
async updateGroupTypeConfig(appId, groupType, data, expectedModifiedAt) {
|
|
1435
|
+
const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
|
|
1436
|
+
return this.patch(`/app/${appId}/api/group-type-configs/${groupType}`, body);
|
|
1437
|
+
}
|
|
1438
|
+
async deleteGroupTypeConfig(appId, groupType) {
|
|
1439
|
+
return this.delete(`/app/${appId}/api/group-type-configs/${groupType}`);
|
|
1440
|
+
}
|
|
1441
|
+
// ============================================
|
|
1442
|
+
// DOCUMENT EXPORT / IMPORT
|
|
1443
|
+
// ============================================
|
|
1444
|
+
async exportDocumentState(appId, documentId) {
|
|
1445
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/export/state`);
|
|
1446
|
+
}
|
|
1447
|
+
async importDocumentState(appId, documentId, stateBase64) {
|
|
1448
|
+
return this.post(`/app/${appId}/api/documents/${documentId}/import/state`, { state: stateBase64 });
|
|
1449
|
+
}
|
|
1450
|
+
async getDocument(appId, documentId) {
|
|
1451
|
+
return this.get(`/app/${appId}/api/documents/${documentId}`);
|
|
1452
|
+
}
|
|
1453
|
+
async createDocument(appId, data) {
|
|
1454
|
+
return this.post(`/app/${appId}/api/documents`, data);
|
|
1455
|
+
}
|
|
1456
|
+
async listDocumentPermissions(appId, documentId) {
|
|
1457
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/permissions`);
|
|
1458
|
+
}
|
|
1459
|
+
async grantDocumentPermission(appId, documentId, permissions) {
|
|
1460
|
+
return this.put(`/app/${appId}/api/documents/${documentId}/permissions`, { permissions });
|
|
1461
|
+
}
|
|
1462
|
+
async listDocumentInvitations(appId, documentId) {
|
|
1463
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/invitations`);
|
|
1464
|
+
}
|
|
1465
|
+
async listDocumentBlobs(appId, documentId) {
|
|
1466
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/blobs`);
|
|
1467
|
+
}
|
|
1468
|
+
async downloadBlob(appId, documentId, blobId) {
|
|
1469
|
+
const credentials = await this.ensureAuthenticated();
|
|
1470
|
+
const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}/blobs/${blobId}/download`;
|
|
1471
|
+
const headers = {
|
|
1472
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
1473
|
+
};
|
|
1474
|
+
if (credentials.globalAdminAppId) {
|
|
1475
|
+
headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
|
|
1476
|
+
}
|
|
1477
|
+
const response = await fetchWithTLS(url, { headers });
|
|
1478
|
+
if (!response.ok) {
|
|
1479
|
+
throw new ApiError(`Failed to download blob: ${response.statusText}`, response.status);
|
|
1480
|
+
}
|
|
1481
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1482
|
+
return Buffer.from(arrayBuffer);
|
|
1483
|
+
}
|
|
1484
|
+
async uploadBlob(appId, documentId, blobId, data, meta) {
|
|
1485
|
+
const credentials = await this.ensureAuthenticated();
|
|
1486
|
+
const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}/blobs/${blobId}`;
|
|
1487
|
+
const headers = {
|
|
1488
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
1489
|
+
"Content-Type": meta.contentType,
|
|
1490
|
+
"X-Blob-Filename": encodeURIComponent(meta.filename),
|
|
1491
|
+
"X-Blob-Size": String(data.length),
|
|
1492
|
+
"X-Blob-Sha256": meta.sha256,
|
|
1493
|
+
};
|
|
1494
|
+
if (credentials.globalAdminAppId) {
|
|
1495
|
+
headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
|
|
1496
|
+
}
|
|
1497
|
+
const response = await fetchWithTLS(url, {
|
|
1498
|
+
method: "PUT",
|
|
1499
|
+
headers,
|
|
1500
|
+
body: data,
|
|
1501
|
+
});
|
|
1502
|
+
if (!response.ok) {
|
|
1503
|
+
const text = await response.text();
|
|
1504
|
+
throw new ApiError(`Failed to upload blob: ${text}`, response.status);
|
|
1505
|
+
}
|
|
1506
|
+
return response.json();
|
|
1507
|
+
}
|
|
1508
|
+
async listDocumentAliases(appId, documentId) {
|
|
1509
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/aliases`);
|
|
1510
|
+
}
|
|
1511
|
+
async setDocumentAlias(appId, aliasScope, aliasKey, documentId, ownerUserId, mustNotExist) {
|
|
1512
|
+
return this.put(`/app/${appId}/api/document-aliases/${aliasScope}/${encodeURIComponent(aliasKey)}`, {
|
|
1513
|
+
documentId,
|
|
1514
|
+
userId: ownerUserId,
|
|
1515
|
+
mustNotExist,
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
async findUserByEmail(appId, email) {
|
|
1519
|
+
try {
|
|
1520
|
+
const result = await this.get(`/app/${appId}/api/users`, { email });
|
|
1521
|
+
const items = result?.items || (Array.isArray(result) ? result : []);
|
|
1522
|
+
return items[0] || null;
|
|
1523
|
+
}
|
|
1524
|
+
catch {
|
|
1525
|
+
return null;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
async listAdminDocuments(appId, userId) {
|
|
1529
|
+
const result = await this.get(`/admin/api/apps/${appId}/documents`, { userId });
|
|
1530
|
+
return result?.documents || result || [];
|
|
1531
|
+
}
|
|
1532
|
+
// ============================================
|
|
1533
|
+
// DATABASE EXPORT / IMPORT
|
|
1534
|
+
// ============================================
|
|
1535
|
+
async saveDatabaseRecord(appId, databaseId, modelName, id, data) {
|
|
1536
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/save`, {
|
|
1537
|
+
modelName,
|
|
1538
|
+
id,
|
|
1539
|
+
data,
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
async batchDatabaseRecords(appId, databaseId, operations) {
|
|
1543
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/batch`, { operations });
|
|
1544
|
+
}
|
|
1545
|
+
async deleteDatabaseRecord(appId, databaseId, modelName, id) {
|
|
1546
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/delete`, { modelName, id });
|
|
1547
|
+
}
|
|
1548
|
+
async batchDeleteDatabaseRecords(appId, databaseId, operations) {
|
|
1549
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/admin-data/batch`, { operations });
|
|
1550
|
+
}
|
|
1551
|
+
async listDatabaseUniqueConstraints(appId, databaseId) {
|
|
1552
|
+
const result = await this.get(`/app/${appId}/api/databases/${databaseId}/records/unique-constraints`);
|
|
1553
|
+
return result?.constraints || result || [];
|
|
1554
|
+
}
|
|
1555
|
+
async registerDatabaseUniqueConstraint(appId, databaseId, constraint) {
|
|
1556
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/unique-constraint/register`, constraint);
|
|
1557
|
+
}
|
|
1558
|
+
// ============================================
|
|
1559
|
+
// BLOB BUCKETS
|
|
1560
|
+
// ============================================
|
|
1561
|
+
async listBlobBuckets(appId) {
|
|
1562
|
+
return this.get(`/app/${appId}/api/blob-buckets`);
|
|
1563
|
+
}
|
|
1564
|
+
async createBlobBucket(appId, payload) {
|
|
1565
|
+
return this.post(`/app/${appId}/api/blob-buckets`, payload);
|
|
1566
|
+
}
|
|
1567
|
+
async getBlobBucket(appId, bucketIdOrKey) {
|
|
1568
|
+
return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`);
|
|
1569
|
+
}
|
|
1570
|
+
async deleteBlobBucket(appId, bucketIdOrKey) {
|
|
1571
|
+
return this.delete(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`);
|
|
1572
|
+
}
|
|
1573
|
+
async listBucketBlobs(appId, bucketIdOrKey, params) {
|
|
1574
|
+
return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`, params);
|
|
1575
|
+
}
|
|
1576
|
+
async uploadBucketBlob(appId, bucketIdOrKey, data, meta) {
|
|
1577
|
+
const credentials = await this.ensureAuthenticated();
|
|
1578
|
+
const url = `${credentials.serverUrl}/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`;
|
|
1579
|
+
const headers = {
|
|
1580
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
1581
|
+
"Content-Type": meta.contentType,
|
|
1582
|
+
"X-Blob-Filename": encodeURIComponent(meta.filename),
|
|
1583
|
+
};
|
|
1584
|
+
if (meta.tags && meta.tags.length > 0) {
|
|
1585
|
+
headers["X-Blob-Tags"] = JSON.stringify(meta.tags);
|
|
1586
|
+
}
|
|
1587
|
+
if (credentials.globalAdminAppId) {
|
|
1588
|
+
headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
|
|
1589
|
+
}
|
|
1590
|
+
const response = await fetchWithTLS(url, {
|
|
1591
|
+
method: "POST",
|
|
1592
|
+
headers,
|
|
1593
|
+
body: data,
|
|
1594
|
+
});
|
|
1595
|
+
if (!response.ok) {
|
|
1596
|
+
const text = await response.text();
|
|
1597
|
+
throw new ApiError(`Failed to upload blob: ${text}`, response.status);
|
|
1598
|
+
}
|
|
1599
|
+
return response.json();
|
|
1600
|
+
}
|
|
1601
|
+
async downloadBucketBlob(appId, bucketIdOrKey, blobId) {
|
|
1602
|
+
const credentials = await this.ensureAuthenticated();
|
|
1603
|
+
const url = `${credentials.serverUrl}/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}`;
|
|
1604
|
+
const headers = {
|
|
1605
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
1606
|
+
};
|
|
1607
|
+
if (credentials.globalAdminAppId) {
|
|
1608
|
+
headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
|
|
1609
|
+
}
|
|
1610
|
+
const response = await fetchWithTLS(url, { headers });
|
|
1611
|
+
if (!response.ok) {
|
|
1612
|
+
throw new ApiError(`Failed to download blob: ${response.statusText}`, response.status);
|
|
1613
|
+
}
|
|
1614
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1615
|
+
return Buffer.from(arrayBuffer);
|
|
1616
|
+
}
|
|
1617
|
+
async deleteBucketBlob(appId, bucketIdOrKey, blobId) {
|
|
1618
|
+
return this.delete(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}`);
|
|
1619
|
+
}
|
|
1620
|
+
async getBucketBlobSignedUrl(appId, bucketIdOrKey, blobId, expiresInSeconds) {
|
|
1621
|
+
return this.post(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}/signed-url`, {
|
|
1622
|
+
expiresInSeconds: expiresInSeconds || 300,
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
766
1625
|
}
|
|
767
1626
|
// Export a singleton instance
|
|
768
1627
|
export const apiClient = new ApiClient();
|