@globio/cli 0.1.6 → 0.1.8

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 CHANGED
@@ -35,6 +35,12 @@ npx @globio/cli migrate firebase-storage \
35
35
  --all
36
36
  ```
37
37
 
38
+ After migration, GlobalDoc indexes are created automatically for every field in your collections.
39
+ Queries using `where()` clauses will work immediately.
40
+
41
+ Note: GlobalDoc requires explicit indexes unlike Firestore's automatic indexing.
42
+ The migrate command handles this for you automatically.
43
+
38
44
  ## Commands
39
45
 
40
46
  ### Auth
package/dist/index.js CHANGED
@@ -446,6 +446,50 @@ import * as p3 from "@clack/prompts";
446
446
  import chalk7 from "chalk";
447
447
  import { basename } from "path";
448
448
 
449
+ // src/lib/api.ts
450
+ var BASE_URL = "https://api.globio.stanlink.online";
451
+ async function apiCall(path2, options = {}) {
452
+ const profileName = options.profile ?? config.getActiveProfile();
453
+ const profile = config.getProfile(profileName);
454
+ if (!profile?.project_api_key) {
455
+ throw new Error("No active project. Run: globio projects use <id>");
456
+ }
457
+ const res = await fetch(`${BASE_URL}${path2}`, {
458
+ method: options.method ?? "GET",
459
+ headers: {
460
+ "Content-Type": "application/json",
461
+ "X-Globio-Key": profile.project_api_key
462
+ },
463
+ body: options.body ? JSON.stringify(options.body) : void 0
464
+ });
465
+ const data = await res.json();
466
+ if (!data.success) {
467
+ throw new Error(data.error ?? `API error ${res.status}`);
468
+ }
469
+ return data;
470
+ }
471
+ async function docSet(collection, docId, data, profile) {
472
+ await apiCall(`/doc/${collection}/${docId}`, {
473
+ method: "PUT",
474
+ body: data,
475
+ profile
476
+ });
477
+ }
478
+ async function createIndex(collection, field, fieldType = "string", profile) {
479
+ void fieldType;
480
+ try {
481
+ await apiCall(`/doc/${collection}/indexes`, {
482
+ method: "POST",
483
+ body: {
484
+ field_path: field,
485
+ index_type: "asc"
486
+ },
487
+ profile
488
+ });
489
+ } catch {
490
+ }
491
+ }
492
+
449
493
  // src/lib/firebase.ts
450
494
  async function initFirebase(serviceAccountPath) {
451
495
  const admin = await import("firebase-admin");
@@ -480,17 +524,6 @@ function createProgressBar(label) {
480
524
  return bar;
481
525
  }
482
526
 
483
- // src/lib/sdk.ts
484
- import { Globio } from "@globio/sdk";
485
- function getClient(profileName) {
486
- const { pat } = config.requireAuth(profileName);
487
- const { projectId } = config.requireProject(profileName);
488
- const profile = config.getProfile(profileName);
489
- const apiKey = profile?.project_api_key ?? pat;
490
- void projectId;
491
- return new Globio({ apiKey });
492
- }
493
-
494
527
  // src/commands/migrate.ts
495
528
  var version2 = getCliVersion();
496
529
  function resolveProfileName(profile) {
@@ -500,7 +533,7 @@ async function migrateFirestore(options) {
500
533
  printBanner(version2);
501
534
  p3.intro(gold("\u21D2\u21D2") + " Firebase \u2192 Globio Migration");
502
535
  const { firestore } = await initFirebase(options.from);
503
- const client = getClient(resolveProfileName(options.profile));
536
+ const profileName = resolveProfileName(options.profile);
504
537
  let collections = [];
505
538
  if (options.all) {
506
539
  const snapshot = await firestore.listCollections();
@@ -531,6 +564,8 @@ async function migrateFirestore(options) {
531
564
  };
532
565
  let lastDoc = null;
533
566
  let processed = 0;
567
+ let firstDocData = null;
568
+ let indexFieldCount = 0;
534
569
  while (processed < total) {
535
570
  let query = firestore.collection(collectionId).limit(100);
536
571
  if (lastDoc) {
@@ -542,10 +577,15 @@ async function migrateFirestore(options) {
542
577
  }
543
578
  for (const doc of snapshot.docs) {
544
579
  try {
545
- const result = await client.doc.set(collectionId, doc.id, doc.data());
546
- if (!result.success) {
547
- throw new Error(result.error.message);
580
+ if (!firstDocData) {
581
+ firstDocData = doc.data();
582
+ for (const [field, value] of Object.entries(firstDocData)) {
583
+ const fieldType = typeof value === "number" ? "number" : typeof value === "boolean" ? "boolean" : "string";
584
+ await createIndex(collectionId, field, fieldType, profileName);
585
+ }
586
+ indexFieldCount = Object.keys(firstDocData).length;
548
587
  }
588
+ await docSet(collectionId, doc.id, doc.data(), profileName);
549
589
  results[collectionId].success++;
550
590
  } catch {
551
591
  results[collectionId].failed++;
@@ -560,6 +600,11 @@ async function migrateFirestore(options) {
560
600
  console.log(
561
601
  chalk7.green(` \u2713 ${results[collectionId].success} documents migrated`)
562
602
  );
603
+ if (indexFieldCount > 0) {
604
+ console.log(
605
+ chalk7.gray(` Indexes created for ${indexFieldCount} fields`)
606
+ );
607
+ }
563
608
  if (results[collectionId].failed > 0) {
564
609
  console.log(chalk7.red(` \u2717 ${results[collectionId].failed} failed`));
565
610
  console.log(
@@ -578,7 +623,11 @@ async function migrateFirebaseStorage(options) {
578
623
  printBanner(version2);
579
624
  p3.intro(gold("\u21D2\u21D2") + " Firebase \u2192 Globio Migration");
580
625
  const { storage } = await initFirebase(options.from);
581
- const client = getClient(resolveProfileName(options.profile));
626
+ const profileName = resolveProfileName(options.profile);
627
+ const profile = config.getProfile(profileName);
628
+ if (!profile?.project_api_key) {
629
+ throw new Error("No active project. Run: globio projects use <id>");
630
+ }
582
631
  const bucketName = options.bucket.replace(/^gs:\/\//, "");
583
632
  const bucket = storage.bucket(bucketName);
584
633
  const prefix = options.folder ? options.folder.replace(/^\//, "") : "";
@@ -591,17 +640,26 @@ async function migrateFirebaseStorage(options) {
591
640
  for (const file of files) {
592
641
  try {
593
642
  const [buffer] = await file.download();
594
- const uploadFile = new File(
595
- [new Uint8Array(buffer)],
643
+ const bytes = Uint8Array.from(buffer);
644
+ const formData = new FormData();
645
+ formData.append(
646
+ "file",
647
+ new Blob([bytes]),
596
648
  basename(file.name) || file.name
597
649
  );
598
- const result = await client.vault.uploadFile(uploadFile, {
599
- metadata: {
600
- original_path: file.name
650
+ formData.append("path", file.name);
651
+ const res = await fetch(
652
+ "https://api.globio.stanlink.online/vault/files",
653
+ {
654
+ method: "POST",
655
+ headers: {
656
+ "X-Globio-Key": profile.project_api_key
657
+ },
658
+ body: formData
601
659
  }
602
- });
603
- if (!result.success) {
604
- throw new Error(result.error.message);
660
+ );
661
+ if (!res.ok) {
662
+ throw new Error(`Upload failed: ${res.status}`);
605
663
  }
606
664
  success++;
607
665
  } catch {
@@ -887,6 +945,19 @@ async function servicesList(options = {}) {
887
945
  import chalk11 from "chalk";
888
946
  import ora from "ora";
889
947
  import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
948
+
949
+ // src/lib/sdk.ts
950
+ import { Globio } from "@globio/sdk";
951
+ function getClient(profileName) {
952
+ const { pat } = config.requireAuth(profileName);
953
+ const { projectId } = config.requireProject(profileName);
954
+ const profile = config.getProfile(profileName);
955
+ const apiKey = profile?.project_api_key ?? pat;
956
+ void projectId;
957
+ return new Globio({ apiKey });
958
+ }
959
+
960
+ // src/commands/functions.ts
890
961
  function resolveProfileName3(profile) {
891
962
  return profile ?? config.getActiveProfile() ?? "default";
892
963
  }
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globio/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "license": "MIT",
5
5
  "exports": "./src/index.ts"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globio/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "The official CLI for Globio — game backend as a service",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -8,9 +8,9 @@ import {
8
8
  orange,
9
9
  printBanner,
10
10
  } from '../lib/banner.js';
11
+ import { createIndex, docSet } from '../lib/api.js';
11
12
  import { initFirebase } from '../lib/firebase.js';
12
13
  import { createProgressBar } from '../lib/progress.js';
13
- import { getClient } from '../lib/sdk.js';
14
14
  import { config } from '../lib/config.js';
15
15
 
16
16
  const version = getCliVersion();
@@ -39,7 +39,7 @@ export async function migrateFirestore(options: MigrateFirestoreOptions) {
39
39
  p.intro(gold('⇒⇒') + ' Firebase → Globio Migration');
40
40
 
41
41
  const { firestore } = await initFirebase(options.from);
42
- const client = getClient(resolveProfileName(options.profile));
42
+ const profileName = resolveProfileName(options.profile);
43
43
 
44
44
  let collections: string[] = [];
45
45
 
@@ -81,6 +81,8 @@ export async function migrateFirestore(options: MigrateFirestoreOptions) {
81
81
 
82
82
  let lastDoc: unknown = null;
83
83
  let processed = 0;
84
+ let firstDocData: Record<string, unknown> | null = null;
85
+ let indexFieldCount = 0;
84
86
 
85
87
  while (processed < total) {
86
88
  let query = firestore.collection(collectionId).limit(100);
@@ -96,10 +98,21 @@ export async function migrateFirestore(options: MigrateFirestoreOptions) {
96
98
 
97
99
  for (const doc of snapshot.docs) {
98
100
  try {
99
- const result = await client.doc.set(collectionId, doc.id, doc.data());
100
- if (!result.success) {
101
- throw new Error(result.error.message);
101
+ if (!firstDocData) {
102
+ firstDocData = doc.data();
103
+ for (const [field, value] of Object.entries(firstDocData)) {
104
+ const fieldType =
105
+ typeof value === 'number'
106
+ ? 'number'
107
+ : typeof value === 'boolean'
108
+ ? 'boolean'
109
+ : 'string';
110
+ await createIndex(collectionId, field, fieldType, profileName);
111
+ }
112
+ indexFieldCount = Object.keys(firstDocData).length;
102
113
  }
114
+
115
+ await docSet(collectionId, doc.id, doc.data(), profileName);
103
116
  results[collectionId].success++;
104
117
  } catch {
105
118
  results[collectionId].failed++;
@@ -117,6 +130,11 @@ export async function migrateFirestore(options: MigrateFirestoreOptions) {
117
130
  console.log(
118
131
  chalk.green(` ✓ ${results[collectionId].success} documents migrated`)
119
132
  );
133
+ if (indexFieldCount > 0) {
134
+ console.log(
135
+ chalk.gray(` Indexes created for ${indexFieldCount} fields`)
136
+ );
137
+ }
120
138
  if (results[collectionId].failed > 0) {
121
139
  console.log(chalk.red(` ✗ ${results[collectionId].failed} failed`));
122
140
  console.log(
@@ -146,7 +164,12 @@ export async function migrateFirebaseStorage(options: MigrateStorageOptions) {
146
164
  p.intro(gold('⇒⇒') + ' Firebase → Globio Migration');
147
165
 
148
166
  const { storage } = await initFirebase(options.from);
149
- const client = getClient(resolveProfileName(options.profile));
167
+ const profileName = resolveProfileName(options.profile);
168
+ const profile = config.getProfile(profileName);
169
+
170
+ if (!profile?.project_api_key) {
171
+ throw new Error('No active project. Run: globio projects use <id>');
172
+ }
150
173
 
151
174
  const bucketName = options.bucket.replace(/^gs:\/\//, '');
152
175
  const bucket = storage.bucket(bucketName);
@@ -165,18 +188,28 @@ export async function migrateFirebaseStorage(options: MigrateStorageOptions) {
165
188
  for (const file of files) {
166
189
  try {
167
190
  const [buffer] = await file.download();
168
- const uploadFile = new File(
169
- [new Uint8Array(buffer)],
191
+ const bytes = Uint8Array.from(buffer);
192
+ const formData = new FormData();
193
+ formData.append(
194
+ 'file',
195
+ new Blob([bytes]),
170
196
  basename(file.name) || file.name
171
197
  );
172
- const result = await client.vault.uploadFile(uploadFile, {
173
- metadata: {
174
- original_path: file.name,
175
- },
176
- });
177
-
178
- if (!result.success) {
179
- throw new Error(result.error.message);
198
+ formData.append('path', file.name);
199
+
200
+ const res = await fetch(
201
+ 'https://api.globio.stanlink.online/vault/files',
202
+ {
203
+ method: 'POST',
204
+ headers: {
205
+ 'X-Globio-Key': profile.project_api_key,
206
+ },
207
+ body: formData,
208
+ }
209
+ );
210
+
211
+ if (!res.ok) {
212
+ throw new Error(`Upload failed: ${res.status}`);
180
213
  }
181
214
 
182
215
  success++;
package/src/lib/api.ts ADDED
@@ -0,0 +1,73 @@
1
+ import { config } from './config.js';
2
+
3
+ const BASE_URL = 'https://api.globio.stanlink.online';
4
+
5
+ export async function apiCall(
6
+ path: string,
7
+ options: {
8
+ method?: string;
9
+ body?: unknown;
10
+ profile?: string;
11
+ } = {}
12
+ ): Promise<unknown> {
13
+ const profileName = options.profile ?? config.getActiveProfile();
14
+ const profile = config.getProfile(profileName);
15
+
16
+ if (!profile?.project_api_key) {
17
+ throw new Error('No active project. Run: globio projects use <id>');
18
+ }
19
+
20
+ const res = await fetch(`${BASE_URL}${path}`, {
21
+ method: options.method ?? 'GET',
22
+ headers: {
23
+ 'Content-Type': 'application/json',
24
+ 'X-Globio-Key': profile.project_api_key,
25
+ },
26
+ body: options.body ? JSON.stringify(options.body) : undefined,
27
+ });
28
+
29
+ const data = (await res.json()) as {
30
+ success: boolean;
31
+ error?: string;
32
+ };
33
+
34
+ if (!data.success) {
35
+ throw new Error(data.error ?? `API error ${res.status}`);
36
+ }
37
+
38
+ return data;
39
+ }
40
+
41
+ export async function docSet(
42
+ collection: string,
43
+ docId: string,
44
+ data: Record<string, unknown>,
45
+ profile?: string
46
+ ): Promise<void> {
47
+ await apiCall(`/doc/${collection}/${docId}`, {
48
+ method: 'PUT',
49
+ body: data,
50
+ profile,
51
+ });
52
+ }
53
+
54
+ export async function createIndex(
55
+ collection: string,
56
+ field: string,
57
+ fieldType: 'string' | 'number' | 'boolean' = 'string',
58
+ profile?: string
59
+ ): Promise<void> {
60
+ void fieldType;
61
+ try {
62
+ await apiCall(`/doc/${collection}/indexes`, {
63
+ method: 'POST',
64
+ body: {
65
+ field_path: field,
66
+ index_type: 'asc',
67
+ },
68
+ profile,
69
+ });
70
+ } catch {
71
+ // Index may already exist. Ignore duplicate-style failures.
72
+ }
73
+ }