@apito-io/js-admin-sdk 2.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts CHANGED
@@ -11,24 +11,44 @@ import {
11
11
  ApitoError,
12
12
  ValidationError,
13
13
  InjectedDBOperationInterface,
14
- TenantLoginResponse,
15
- TenantUser,
16
- TenantUsersResponse,
14
+ LoginUserResponse,
15
+ User,
16
+ UsersResponse,
17
17
  TenantByDomainResponse,
18
18
  TenantCatalogSearchRow,
19
+ LoginUserParams,
20
+ CreateUserParams,
21
+ UpdateUserParams,
22
+ GoogleOAuthStateResponse,
23
+ ProjectStorageSettings,
24
+ UpdateProjectStorageInput,
25
+ SystemFile,
26
+ SystemFilesListResponse,
27
+ SystemFileUploadParams,
28
+ DeleteSystemFilesResponse,
19
29
  } from './types';
20
30
 
31
+ function deriveRestBaseURL(graphqlURL: string): string {
32
+ const u = graphqlURL.trim().replace(/\/$/, '');
33
+ if (u.endsWith('/graphql')) {
34
+ return u.slice(0, -'/graphql'.length);
35
+ }
36
+ return u;
37
+ }
38
+
21
39
  /**
22
40
  * Apito SDK Client - JavaScript implementation matching the Go SDK
23
41
  */
24
42
  export class ApitoClient implements InjectedDBOperationInterface {
25
43
  private httpClient: AxiosInstance;
26
44
  private baseURL: string;
45
+ private restBaseURL: string;
27
46
  private apiKey: string;
28
47
  private tenantId?: string;
29
48
 
30
49
  constructor(config: ClientConfig) {
31
50
  this.baseURL = config.baseURL;
51
+ this.restBaseURL = (config.restBaseURL ?? '').trim() || deriveRestBaseURL(config.baseURL);
32
52
  this.apiKey = config.apiKey;
33
53
  this.tenantId = config.tenantId;
34
54
 
@@ -144,22 +164,110 @@ export class ApitoClient implements InjectedDBOperationInterface {
144
164
  return data.token;
145
165
  }
146
166
 
167
+ private authHeaders(tenantId?: string): Record<string, string> {
168
+ const headers: Record<string, string> = {
169
+ ...(this.apiKey.startsWith('cli-') || this.apiKey.startsWith('sdk-')
170
+ ? { 'X-Apito-Sync-Key': this.apiKey }
171
+ : { 'X-Apito-Key': this.apiKey }),
172
+ };
173
+ const tid = tenantId ?? this.tenantId;
174
+ if (tid) {
175
+ headers['X-Apito-Tenant-ID'] = tid;
176
+ }
177
+ return headers;
178
+ }
179
+
180
+ private async executeREST<T>(
181
+ method: 'GET' | 'POST',
182
+ path: string,
183
+ options?: {
184
+ query?: Record<string, string | number | undefined>;
185
+ jsonBody?: Record<string, unknown>;
186
+ formData?: FormData;
187
+ allowFailure?: boolean;
188
+ }
189
+ ): Promise<T> {
190
+ const url = new URL(`${this.restBaseURL.replace(/\/$/, '')}${path}`);
191
+ if (options?.query) {
192
+ for (const [k, v] of Object.entries(options.query)) {
193
+ if (v !== undefined && v !== '') {
194
+ url.searchParams.set(k, String(v));
195
+ }
196
+ }
197
+ }
198
+ const headers = this.authHeaders();
199
+ let data: FormData | Record<string, unknown> | undefined;
200
+ if (options?.formData) {
201
+ data = options.formData;
202
+ } else if (options?.jsonBody) {
203
+ headers['Content-Type'] = 'application/json';
204
+ data = options.jsonBody;
205
+ }
206
+ try {
207
+ const response = await this.httpClient.request({
208
+ method,
209
+ url: url.toString(),
210
+ headers,
211
+ data,
212
+ maxBodyLength: Infinity,
213
+ maxContentLength: Infinity,
214
+ });
215
+ const body = response.data as Record<string, unknown>;
216
+ if (body.success === false && !options?.allowFailure) {
217
+ throw new ValidationError(String(body.message ?? 'request failed'));
218
+ }
219
+ return body as T;
220
+ } catch (error) {
221
+ if (axios.isAxiosError(error)) {
222
+ const msg =
223
+ (error.response?.data as { message?: string })?.message ?? error.message;
224
+ throw new ApitoError(msg, 'HTTP_ERROR', error.response?.status, error.response?.data);
225
+ }
226
+ throw error;
227
+ }
228
+ }
229
+
147
230
  /**
148
- * Tenant catalog login (system GraphQL). Returns a tenant-scoped `ak_` API token on success.
231
+ * Project user login (system GraphQL `loginUser`). Password path: pass `password` and `email` or `phone` per project Authentication settings. Google OAuth path: `authMethod: 'google'` with `code` and `state` from the redirect; call `googleOAuthState(projectId)` before opening Google to obtain `state`.
149
232
  */
150
- async loginTenantUser(
151
- username: string,
152
- password: string,
153
- projectId: string
154
- ): Promise<TenantLoginResponse> {
233
+ async loginUser(params: LoginUserParams): Promise<LoginUserResponse> {
234
+ const authMethod = (params.authMethod ?? 'general').trim().toLowerCase() || 'general';
235
+ const variables: Record<string, any> = {
236
+ project_id: params.projectId,
237
+ };
238
+
239
+ if (authMethod === 'google') {
240
+ variables.auth_method = 'google';
241
+ const code = (params.code ?? '').trim();
242
+ const state = (params.state ?? '').trim();
243
+ if (!code || !state) {
244
+ throw new ValidationError('code and state are required for Google login');
245
+ }
246
+ variables.code = code;
247
+ variables.state = state;
248
+ } else {
249
+ const password = (params.password ?? '').trim();
250
+ if (!password) {
251
+ throw new ValidationError('password is required');
252
+ }
253
+ variables.password = password;
254
+ const email = (params.email ?? '').trim();
255
+ const phone = (params.phone ?? '').trim();
256
+ if (!email && !phone) {
257
+ throw new ValidationError('email or phone is required');
258
+ }
259
+ if (email) variables.email = email;
260
+ if (phone) variables.phone = phone;
261
+ }
262
+
155
263
  const query = `
156
- query LoginTenantUser($project_id: String!, $username: String!, $password: String!) {
157
- loginTenantUser(project_id: $project_id, username: $username, password: $password) {
264
+ query LoginUser($project_id: String!, $password: String, $auth_method: String, $email: String, $phone: String, $code: String, $state: String) {
265
+ loginUser(project_id: $project_id, password: $password, auth_method: $auth_method, email: $email, phone: $phone, code: $code, state: $state) {
158
266
  token
159
267
  user {
160
268
  id
161
- username
162
269
  email
270
+ phone
163
271
  role
164
272
  provider
165
273
  tenant_id
@@ -170,68 +278,54 @@ export class ApitoClient implements InjectedDBOperationInterface {
170
278
  }
171
279
  }
172
280
  `;
173
- const variables: Record<string, any> = { project_id: projectId, username, password };
174
281
  const response = await this.executeGraphQL(query, variables);
175
- const raw = response.data?.loginTenantUser;
282
+ const raw = response.data?.loginUser;
176
283
  if (!raw?.token) {
177
- throw new ValidationError('Invalid response format for loginTenantUser');
284
+ throw new ValidationError('Invalid response format for loginUser');
178
285
  }
179
286
  return {
180
287
  token: raw.token as string,
181
- user: raw.user as TenantUser | undefined,
288
+ user: raw.user as User | undefined,
182
289
  };
183
290
  }
184
291
 
185
292
  /**
186
- * Google ID token login for tenant users (project must have google_client_id set).
293
+ * Signed OAuth state for Google login (system query `googleOAuthState`). Use in the authorize URL together with project `google_client_id` and the configured redirect URI.
187
294
  */
188
- async loginTenantUserGoogle(projectId: string, idToken: string): Promise<TenantLoginResponse> {
295
+ async googleOAuthState(projectId: string): Promise<GoogleOAuthStateResponse> {
189
296
  const query = `
190
- mutation LoginTenantUserGoogle($project_id: String!, $id_token: String!) {
191
- loginTenantUserGoogle(project_id: $project_id, id_token: $id_token) {
192
- token
193
- user {
194
- id
195
- username
196
- email
197
- role
198
- provider
199
- tenant_id
200
- status
201
- created_at
202
- updated_at
203
- }
297
+ query GoogleOAuthState($project_id: String!) {
298
+ googleOAuthState(project_id: $project_id) {
299
+ state
204
300
  }
205
301
  }
206
302
  `;
207
- const variables = { project_id: projectId, id_token: idToken };
303
+ const variables = { project_id: projectId };
208
304
  const response = await this.executeGraphQL(query, variables);
209
- const raw = response.data?.loginTenantUserGoogle;
210
- if (!raw?.token) {
211
- throw new ValidationError('Invalid response format for loginTenantUserGoogle');
305
+ const raw = response.data?.googleOAuthState;
306
+ const state = typeof raw?.state === 'string' ? raw.state.trim() : '';
307
+ if (!state) {
308
+ throw new ValidationError('Invalid response format for googleOAuthState');
212
309
  }
213
- return {
214
- token: raw.token as string,
215
- user: raw.user as TenantUser | undefined,
216
- };
310
+ return { state };
217
311
  }
218
312
 
219
313
  /**
220
- * Search tenant users for a project.
314
+ * Search project end-users.
221
315
  */
222
- async searchTenantUsers(
316
+ async searchUsers(
223
317
  projectId: string,
224
318
  limit?: number,
225
319
  offset?: number
226
- ): Promise<TenantUsersResponse> {
320
+ ): Promise<UsersResponse> {
227
321
  const query = `
228
- query SearchTenantUsers($project_id: String!, $limit: Int, $offset: Int) {
229
- searchTenantUsers(project_id: $project_id, limit: $limit, offset: $offset) {
322
+ query SearchUsers($project_id: String!, $limit: Int, $offset: Int) {
323
+ searchUsers(project_id: $project_id, limit: $limit, offset: $offset) {
230
324
  count
231
325
  users {
232
326
  id
233
- username
234
327
  email
328
+ phone
235
329
  role
236
330
  provider
237
331
  tenant_id
@@ -246,15 +340,15 @@ export class ApitoClient implements InjectedDBOperationInterface {
246
340
  if (limit !== undefined) variables.limit = limit;
247
341
  if (offset !== undefined) variables.offset = offset;
248
342
  const response = await this.executeGraphQL(query, variables);
249
- const raw = response.data?.searchTenantUsers;
343
+ const raw = response.data?.searchUsers;
250
344
  if (!raw) {
251
- throw new ValidationError('Invalid response format for searchTenantUsers');
345
+ throw new ValidationError('Invalid response format for searchUsers');
252
346
  }
253
347
  let count = 0;
254
348
  if (typeof raw.count === 'number') {
255
349
  count = raw.count;
256
350
  }
257
- const users = (raw.users ?? []) as TenantUser[];
351
+ const users = (raw.users ?? []) as User[];
258
352
  return { users, count };
259
353
  }
260
354
 
@@ -289,21 +383,63 @@ export class ApitoClient implements InjectedDBOperationInterface {
289
383
  }
290
384
 
291
385
  /**
292
- * Create a tenant catalog user (local password).
386
+ * Create a project user (local password). Use `email` and/or `phone` per engine validation for the project identifier mode.
293
387
  */
294
- async createTenantUser(
295
- projectId: string,
296
- username: string,
297
- email: string,
298
- password: string,
299
- role: string
300
- ): Promise<TenantUser> {
388
+ async createUser(projectId: string, params: CreateUserParams): Promise<User> {
389
+ const password = (params.password ?? '').trim();
390
+ if (!password) {
391
+ throw new ValidationError('password is required');
392
+ }
393
+ const query = `
394
+ mutation CreateUser($project_id: String!, $password: String!, $role: String, $email: String, $phone: String) {
395
+ createUser(project_id: $project_id, password: $password, role: $role, email: $email, phone: $phone) {
396
+ id
397
+ email
398
+ phone
399
+ role
400
+ provider
401
+ tenant_id
402
+ status
403
+ created_at
404
+ updated_at
405
+ }
406
+ }
407
+ `;
408
+ const variables: Record<string, any> = {
409
+ project_id: projectId,
410
+ password,
411
+ };
412
+ const role = (params.role ?? '').trim();
413
+ if (role) variables.role = role;
414
+ const email = (params.email ?? '').trim();
415
+ if (email) variables.email = email;
416
+ const phone = (params.phone ?? '').trim();
417
+ if (phone) variables.phone = phone;
418
+ const response = await this.executeGraphQL(query, variables);
419
+ const u = response.data?.createUser;
420
+ if (!u?.id) {
421
+ throw new ValidationError('Invalid response format for createUser');
422
+ }
423
+ return u as User;
424
+ }
425
+
426
+ /**
427
+ * Update a project user. Project scope is implied by the API key. Only include fields to change.
428
+ */
429
+ async updateUser(userId: string, params: UpdateUserParams): Promise<User> {
430
+ const uid = (userId ?? '').trim();
431
+ if (!uid) {
432
+ throw new ValidationError('userId is required');
433
+ }
434
+ if (params.email === undefined && params.phone === undefined && params.role === undefined) {
435
+ throw new ValidationError('at least one field must be provided');
436
+ }
301
437
  const query = `
302
- mutation CreateTenantUser($project_id: String!, $username: String!, $password: String!, $role: String, $email: String) {
303
- createTenantUser(project_id: $project_id, username: $username, password: $password, role: $role, email: $email) {
438
+ mutation UpdateUser($user_id: String!, $email: String, $phone: String, $role: String) {
439
+ updateUser(user_id: $user_id, email: $email, phone: $phone, role: $role) {
304
440
  id
305
- username
306
441
  email
442
+ phone
307
443
  role
308
444
  provider
309
445
  tenant_id
@@ -313,13 +449,183 @@ export class ApitoClient implements InjectedDBOperationInterface {
313
449
  }
314
450
  }
315
451
  `;
316
- const variables = { project_id: projectId, username, password, email, role };
452
+ const variables: Record<string, any> = { user_id: uid };
453
+ if (params.email !== undefined) variables.email = params.email;
454
+ if (params.phone !== undefined) variables.phone = params.phone;
455
+ if (params.role !== undefined) variables.role = params.role;
317
456
  const response = await this.executeGraphQL(query, variables);
318
- const u = response.data?.createTenantUser;
457
+ const u = response.data?.updateUser;
319
458
  if (!u?.id) {
320
- throw new ValidationError('Invalid response format for createTenantUser');
459
+ throw new ValidationError('Invalid response format for updateUser');
460
+ }
461
+ return u as User;
462
+ }
463
+
464
+ /** Set a new password for a project user (admin mutation resetUserPassword). */
465
+ async resetUserPassword(userId: string, password: string): Promise<boolean> {
466
+ const uid = (userId ?? '').trim();
467
+ if (!uid) {
468
+ throw new ValidationError('userId is required');
469
+ }
470
+ if (!(password ?? '').trim()) {
471
+ throw new ValidationError('password is required');
472
+ }
473
+ const query = `
474
+ mutation ResetUserPassword($user_id: String!, $password: String!) {
475
+ resetUserPassword(user_id: $user_id, password: $password)
476
+ }
477
+ `;
478
+ const response = await this.executeGraphQL(query, { user_id: uid, password });
479
+ const ok = response.data?.resetUserPassword;
480
+ if (typeof ok !== 'boolean') {
481
+ throw new ValidationError('Invalid response format for resetUserPassword');
482
+ }
483
+ return ok;
484
+ }
485
+
486
+ /**
487
+ * Delete a project user by id. Project scope is implied by the API key.
488
+ */
489
+ async deleteUser(userId: string): Promise<boolean> {
490
+ const uid = (userId ?? '').trim();
491
+ if (!uid) {
492
+ throw new ValidationError('userId is required');
493
+ }
494
+ const query = `
495
+ mutation DeleteUser($user_id: String!) {
496
+ deleteUser(user_id: $user_id)
497
+ }
498
+ `;
499
+ const response = await this.executeGraphQL(query, { user_id: uid });
500
+ const ok = response.data?.deleteUser;
501
+ if (typeof ok !== 'boolean') {
502
+ throw new ValidationError('Invalid response format for deleteUser');
503
+ }
504
+ return ok;
505
+ }
506
+
507
+ /** Read project storage settings via getProject. */
508
+ async getProjectStorageSettings(projectId: string): Promise<ProjectStorageSettings> {
509
+ const query = `
510
+ query GetProjectStorageSettings($_id: String!) {
511
+ getProject(_id: $_id) {
512
+ storage_settings {
513
+ use_free_cloud_storage
514
+ endpoint
515
+ region
516
+ bucket
517
+ access_key_id
518
+ has_secret_access_key
519
+ public_base_url
520
+ force_path_style
521
+ }
522
+ }
523
+ }
524
+ `;
525
+ const response = await this.executeGraphQL(query, { _id: projectId });
526
+ const settings = response.data?.getProject?.storage_settings;
527
+ if (!settings) {
528
+ throw new ValidationError('Invalid response format for getProjectStorageSettings');
529
+ }
530
+ return settings as ProjectStorageSettings;
531
+ }
532
+
533
+ /** Persist project storage settings. */
534
+ async updateProjectStorageSettings(
535
+ input: UpdateProjectStorageInput
536
+ ): Promise<ProjectStorageSettings> {
537
+ const query = `
538
+ mutation UpdateProjectStorageSettings($input: UpdateProjectStorageInput!) {
539
+ updateProjectStorageSettings(input: $input) {
540
+ storage_settings {
541
+ use_free_cloud_storage
542
+ endpoint
543
+ region
544
+ bucket
545
+ access_key_id
546
+ has_secret_access_key
547
+ public_base_url
548
+ force_path_style
549
+ }
550
+ }
551
+ }
552
+ `;
553
+ const response = await this.executeGraphQL(query, { input });
554
+ const settings = response.data?.updateProjectStorageSettings?.storage_settings;
555
+ if (!settings) {
556
+ throw new ValidationError('Invalid response format for updateProjectStorageSettings');
557
+ }
558
+ return settings as ProjectStorageSettings;
559
+ }
560
+
561
+ /** Upload a file via POST /system/files/upload. */
562
+ async uploadSystemFile(params: SystemFileUploadParams): Promise<SystemFile> {
563
+ const size =
564
+ params.content instanceof ArrayBuffer
565
+ ? params.content.byteLength
566
+ : params.content.byteLength;
567
+ if (!params.content || size === 0) {
568
+ throw new ValidationError('file content is required');
569
+ }
570
+ const fileName = (params.fileName ?? '').trim() || 'upload';
571
+ const form = new FormData();
572
+ const bytes =
573
+ params.content instanceof ArrayBuffer ? new Uint8Array(params.content) : params.content;
574
+ const blob = new Blob([bytes as BlobPart]);
575
+ form.append('file', blob, fileName);
576
+ if (params.fileType?.trim()) {
577
+ form.append('file_type', params.fileType.trim());
578
+ }
579
+ const body = await this.executeREST<{ file: SystemFile }>('POST', '/files/upload', {
580
+ formData: form,
581
+ });
582
+ if (!body.file?.id) {
583
+ throw new ValidationError('Invalid response format for uploadSystemFile');
584
+ }
585
+ return body.file;
586
+ }
587
+
588
+ /** List files via GET /system/files/list. */
589
+ async listSystemFiles(
590
+ fileType?: string,
591
+ limit?: number,
592
+ offset?: number
593
+ ): Promise<SystemFilesListResponse> {
594
+ const body = await this.executeREST<{
595
+ files: SystemFile[];
596
+ total: number;
597
+ }>('GET', '/files/list', {
598
+ query: {
599
+ file_type: fileType,
600
+ limit,
601
+ offset,
602
+ },
603
+ });
604
+ return {
605
+ files: body.files ?? [],
606
+ total: body.total ?? 0,
607
+ };
608
+ }
609
+
610
+ /** Delete files via POST /system/files/delete. */
611
+ async deleteSystemFiles(ids: string[]): Promise<DeleteSystemFilesResponse> {
612
+ if (!ids?.length) {
613
+ throw new ValidationError('ids are required');
614
+ }
615
+ const body = await this.executeREST<DeleteSystemFilesResponse>('POST', '/files/delete', {
616
+ jsonBody: { ids },
617
+ allowFailure: true,
618
+ });
619
+ const result: DeleteSystemFilesResponse = {
620
+ success: !!body.success,
621
+ deleted_ids: body.deleted_ids ?? [],
622
+ storage_failed: body.storage_failed,
623
+ message: body.message,
624
+ };
625
+ if (!result.success && result.message) {
626
+ throw new ValidationError(result.message);
321
627
  }
322
- return u as TenantUser;
628
+ return result;
323
629
  }
324
630
 
325
631
  /**
package/src/index.ts CHANGED
@@ -24,6 +24,14 @@ export type {
24
24
  ApitoError,
25
25
  ValidationError,
26
26
  InjectedDBOperationInterface,
27
+ LoginUserParams,
28
+ CreateUserParams,
29
+ UpdateUserParams,
30
+ User,
31
+ ProjectStorageSettings,
32
+ UpdateProjectStorageInput,
33
+ SystemFile,
34
+ SystemFileUploadParams,
27
35
  } from './types';
28
36
 
29
37
  // Default export for convenience