@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 +6 -0
- package/dist/index.js +95 -24
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/commands/migrate.ts +49 -16
- package/src/lib/api.ts +73 -0
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
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
|
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
|
|
595
|
-
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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 (!
|
|
604
|
-
throw new Error(
|
|
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
package/package.json
CHANGED
package/src/commands/migrate.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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
|
|
169
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
}
|