@hot-updater/firebase 0.21.11 → 0.21.13
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/dist/index.cjs +177 -179
- package/dist/index.d.cts +3 -4
- package/dist/index.d.ts +3 -4
- package/dist/index.js +178 -180
- package/package.json +6 -6
package/dist/index.cjs
CHANGED
|
@@ -45,194 +45,192 @@ const convertToBundle = (firestoreData) => ({
|
|
|
45
45
|
fingerprintHash: firestoreData.fingerprint_hash,
|
|
46
46
|
metadata: firestoreData?.metadata ?? {}
|
|
47
47
|
});
|
|
48
|
-
const firebaseDatabase = (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
for (const doc of querySnapshot.docs) {
|
|
94
|
-
const data = doc.data();
|
|
95
|
-
if (data.name) channels.add(data.name);
|
|
96
|
-
}
|
|
97
|
-
return Array.from(channels);
|
|
98
|
-
},
|
|
99
|
-
async commitBundle(context, { changedSets }) {
|
|
100
|
-
if (changedSets.length === 0) return;
|
|
101
|
-
let isTargetAppVersionChanged = false;
|
|
102
|
-
const deletedBundleIds = /* @__PURE__ */ new Set();
|
|
103
|
-
await context.db.runTransaction(async (transaction) => {
|
|
104
|
-
const bundlesSnapshot = await transaction.get(context.bundlesCollection);
|
|
105
|
-
const targetVersionsSnapshot = await transaction.get(context.db.collection("target_app_versions"));
|
|
106
|
-
const channelsSnapshot = await transaction.get(context.db.collection("channels"));
|
|
107
|
-
const bundlesMap = {};
|
|
108
|
-
for (const doc of bundlesSnapshot.docs) bundlesMap[doc.id] = doc.data();
|
|
109
|
-
for (const { operation, data } of changedSets) {
|
|
110
|
-
if (data.targetAppVersion) isTargetAppVersionChanged = true;
|
|
111
|
-
if (operation === "insert" || operation === "update") {
|
|
112
|
-
bundlesMap[data.id] = {
|
|
113
|
-
id: data.id,
|
|
114
|
-
channel: data.channel,
|
|
115
|
-
enabled: data.enabled,
|
|
116
|
-
should_force_update: data.shouldForceUpdate,
|
|
117
|
-
file_hash: data.fileHash,
|
|
118
|
-
git_commit_hash: data.gitCommitHash || null,
|
|
119
|
-
message: data.message || null,
|
|
120
|
-
platform: data.platform,
|
|
121
|
-
target_app_version: data.targetAppVersion,
|
|
122
|
-
storage_uri: data.storageUri,
|
|
123
|
-
fingerprint_hash: data.fingerprintHash,
|
|
124
|
-
metadata: data.metadata ?? {}
|
|
125
|
-
};
|
|
126
|
-
const channelRef = context.db.collection("channels").doc(data.channel);
|
|
127
|
-
transaction.set(channelRef, { name: data.channel }, { merge: true });
|
|
128
|
-
} else if (operation === "delete") {
|
|
129
|
-
if (!bundlesMap[data.id]) throw new Error(`Bundle with id ${data.id} not found`);
|
|
130
|
-
delete bundlesMap[data.id];
|
|
131
|
-
deletedBundleIds.add(data.id);
|
|
132
|
-
isTargetAppVersionChanged = true;
|
|
133
|
-
}
|
|
48
|
+
const firebaseDatabase = (0, __hot_updater_plugin_core.createDatabasePlugin)({
|
|
49
|
+
name: "firebaseDatabase",
|
|
50
|
+
factory: (config) => {
|
|
51
|
+
let bundles = [];
|
|
52
|
+
let app;
|
|
53
|
+
try {
|
|
54
|
+
app = firebase_admin.app();
|
|
55
|
+
} catch {
|
|
56
|
+
app = firebase_admin.initializeApp(config);
|
|
57
|
+
}
|
|
58
|
+
const db = firebase_admin.firestore(app);
|
|
59
|
+
const bundlesCollection = db.collection("bundles");
|
|
60
|
+
return {
|
|
61
|
+
async getBundleById(bundleId) {
|
|
62
|
+
const found = bundles.find((b) => b.id === bundleId);
|
|
63
|
+
if (found) return found;
|
|
64
|
+
const bundleSnap = await bundlesCollection.doc(bundleId).get();
|
|
65
|
+
if (!bundleSnap.exists) return null;
|
|
66
|
+
return convertToBundle(bundleSnap.data());
|
|
67
|
+
},
|
|
68
|
+
async getBundles(options) {
|
|
69
|
+
const { where, limit, offset } = options;
|
|
70
|
+
let query = bundlesCollection;
|
|
71
|
+
if (where?.channel) query = query.where("channel", "==", where.channel);
|
|
72
|
+
if (where?.platform) query = query.where("platform", "==", where.platform);
|
|
73
|
+
query = query.orderBy("id", "desc");
|
|
74
|
+
const total = (await query.get()).size;
|
|
75
|
+
if (offset > 0) query = query.offset(offset);
|
|
76
|
+
if (limit) query = query.limit(limit);
|
|
77
|
+
bundles = (await query.get()).docs.map((doc) => convertToBundle(doc.data()));
|
|
78
|
+
return {
|
|
79
|
+
data: bundles,
|
|
80
|
+
pagination: (0, __hot_updater_plugin_core.calculatePagination)(total, {
|
|
81
|
+
limit,
|
|
82
|
+
offset
|
|
83
|
+
})
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
async getChannels() {
|
|
87
|
+
const querySnapshot = await db.collection("channels").get();
|
|
88
|
+
if (querySnapshot.empty) return [];
|
|
89
|
+
const channels = /* @__PURE__ */ new Set();
|
|
90
|
+
for (const doc of querySnapshot.docs) {
|
|
91
|
+
const data = doc.data();
|
|
92
|
+
if (data.name) channels.add(data.name);
|
|
134
93
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
94
|
+
return Array.from(channels);
|
|
95
|
+
},
|
|
96
|
+
async commitBundle({ changedSets }) {
|
|
97
|
+
if (changedSets.length === 0) return;
|
|
98
|
+
let isTargetAppVersionChanged = false;
|
|
99
|
+
const deletedBundleIds = /* @__PURE__ */ new Set();
|
|
100
|
+
await db.runTransaction(async (transaction) => {
|
|
101
|
+
const bundlesSnapshot = await transaction.get(bundlesCollection);
|
|
102
|
+
const targetVersionsSnapshot = await transaction.get(db.collection("target_app_versions"));
|
|
103
|
+
const channelsSnapshot = await transaction.get(db.collection("channels"));
|
|
104
|
+
const bundlesMap = {};
|
|
105
|
+
for (const doc of bundlesSnapshot.docs) bundlesMap[doc.id] = doc.data();
|
|
106
|
+
for (const { operation, data } of changedSets) {
|
|
107
|
+
if (data.targetAppVersion) isTargetAppVersionChanged = true;
|
|
108
|
+
if (operation === "insert" || operation === "update") {
|
|
109
|
+
bundlesMap[data.id] = {
|
|
110
|
+
id: data.id,
|
|
111
|
+
channel: data.channel,
|
|
112
|
+
enabled: data.enabled,
|
|
113
|
+
should_force_update: data.shouldForceUpdate,
|
|
114
|
+
file_hash: data.fileHash,
|
|
115
|
+
git_commit_hash: data.gitCommitHash || null,
|
|
116
|
+
message: data.message || null,
|
|
117
|
+
platform: data.platform,
|
|
118
|
+
target_app_version: data.targetAppVersion,
|
|
119
|
+
storage_uri: data.storageUri,
|
|
120
|
+
fingerprint_hash: data.fingerprintHash,
|
|
121
|
+
metadata: data.metadata ?? {}
|
|
122
|
+
};
|
|
123
|
+
const channelRef = db.collection("channels").doc(data.channel);
|
|
124
|
+
transaction.set(channelRef, { name: data.channel }, { merge: true });
|
|
125
|
+
} else if (operation === "delete") {
|
|
126
|
+
if (!bundlesMap[data.id]) throw new Error(`Bundle with id ${data.id} not found`);
|
|
127
|
+
delete bundlesMap[data.id];
|
|
128
|
+
deletedBundleIds.add(data.id);
|
|
129
|
+
isTargetAppVersionChanged = true;
|
|
130
|
+
}
|
|
141
131
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
target_app_version: data.targetAppVersion || null,
|
|
157
|
-
storage_uri: data.storageUri,
|
|
158
|
-
fingerprint_hash: data.fingerprintHash,
|
|
159
|
-
metadata: data.metadata ?? {}
|
|
160
|
-
}, { merge: true });
|
|
161
|
-
if (data.targetAppVersion) {
|
|
162
|
-
const versionDocId = `${data.platform}_${data.channel}_${data.targetAppVersion}`;
|
|
163
|
-
const targetAppVersionsRef = context.db.collection("target_app_versions").doc(versionDocId);
|
|
164
|
-
transaction.set(targetAppVersionsRef, {
|
|
132
|
+
const requiredTargetVersionKeys = /* @__PURE__ */ new Set();
|
|
133
|
+
const requiredChannels = /* @__PURE__ */ new Set();
|
|
134
|
+
for (const bundle of Object.values(bundlesMap)) {
|
|
135
|
+
if (bundle.target_app_version) {
|
|
136
|
+
const key = `${bundle.platform}_${bundle.channel}_${bundle.target_app_version}`;
|
|
137
|
+
requiredTargetVersionKeys.add(key);
|
|
138
|
+
}
|
|
139
|
+
requiredChannels.add(bundle.channel);
|
|
140
|
+
}
|
|
141
|
+
for (const { operation, data } of changedSets) {
|
|
142
|
+
const bundleRef = bundlesCollection.doc(data.id);
|
|
143
|
+
if (operation === "insert" || operation === "update") {
|
|
144
|
+
transaction.set(bundleRef, {
|
|
145
|
+
id: data.id,
|
|
165
146
|
channel: data.channel,
|
|
147
|
+
enabled: data.enabled,
|
|
148
|
+
should_force_update: data.shouldForceUpdate,
|
|
149
|
+
file_hash: data.fileHash,
|
|
150
|
+
git_commit_hash: data.gitCommitHash || null,
|
|
151
|
+
message: data.message || null,
|
|
166
152
|
platform: data.platform,
|
|
167
|
-
target_app_version: data.targetAppVersion
|
|
153
|
+
target_app_version: data.targetAppVersion || null,
|
|
154
|
+
storage_uri: data.storageUri,
|
|
155
|
+
fingerprint_hash: data.fingerprintHash,
|
|
156
|
+
metadata: data.metadata ?? {}
|
|
168
157
|
}, { merge: true });
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
158
|
+
if (data.targetAppVersion) {
|
|
159
|
+
const versionDocId = `${data.platform}_${data.channel}_${data.targetAppVersion}`;
|
|
160
|
+
const targetAppVersionsRef = db.collection("target_app_versions").doc(versionDocId);
|
|
161
|
+
transaction.set(targetAppVersionsRef, {
|
|
162
|
+
channel: data.channel,
|
|
163
|
+
platform: data.platform,
|
|
164
|
+
target_app_version: data.targetAppVersion
|
|
165
|
+
}, { merge: true });
|
|
166
|
+
}
|
|
167
|
+
} else if (operation === "delete") transaction.delete(bundleRef);
|
|
168
|
+
}
|
|
169
|
+
if (isTargetAppVersionChanged) {
|
|
170
|
+
for (const targetDoc of targetVersionsSnapshot.docs) if (!requiredTargetVersionKeys.has(targetDoc.id)) transaction.delete(targetDoc.ref);
|
|
171
|
+
}
|
|
172
|
+
for (const channelDoc of channelsSnapshot.docs) if (!requiredChannels.has(channelDoc.id)) transaction.delete(channelDoc.ref);
|
|
173
|
+
});
|
|
174
|
+
for (const bundleId of deletedBundleIds) bundles = bundles.filter((b) => b.id !== bundleId);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
});
|
|
182
179
|
|
|
183
180
|
//#endregion
|
|
184
181
|
//#region src/firebaseStorage.ts
|
|
185
|
-
const firebaseStorage = (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
name: "firebaseStorage",
|
|
196
|
-
supportedProtocol: "gs",
|
|
197
|
-
async delete(storageUri) {
|
|
198
|
-
const { bucket: bucketName, key } = (0, __hot_updater_plugin_core.parseStorageUri)(storageUri, "gs");
|
|
199
|
-
if (bucketName !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${bucketName}".`);
|
|
200
|
-
try {
|
|
201
|
-
const [files] = await bucket.getFiles({ prefix: key });
|
|
202
|
-
await Promise.all(files.map((file) => file.delete()));
|
|
203
|
-
} catch (e) {
|
|
204
|
-
console.error("Error listing or deleting files:", e);
|
|
205
|
-
throw new Error("Bundle Not Found");
|
|
206
|
-
}
|
|
207
|
-
},
|
|
208
|
-
async upload(key, filePath) {
|
|
209
|
-
try {
|
|
210
|
-
const fileContent = await fs_promises.default.readFile(filePath);
|
|
211
|
-
const contentType = (0, __hot_updater_plugin_core.getContentType)(filePath);
|
|
212
|
-
const storageKey = getStorageKey(key, path.default.basename(filePath));
|
|
213
|
-
await bucket.file(storageKey).save(fileContent, { metadata: { contentType } });
|
|
214
|
-
hooks?.onStorageUploaded?.();
|
|
215
|
-
return { storageUri: `gs://${config.storageBucket}/${storageKey}` };
|
|
216
|
-
} catch (error) {
|
|
217
|
-
console.error("Error uploading bundle:", error);
|
|
218
|
-
if (error instanceof Error) throw new Error(`Failed to upload bundle: ${error.message}`);
|
|
219
|
-
throw error;
|
|
220
|
-
}
|
|
221
|
-
},
|
|
222
|
-
async getDownloadUrl(storageUri) {
|
|
223
|
-
const u = new URL(storageUri);
|
|
224
|
-
if (u.protocol.replace(":", "") !== "gs") throw new Error("Invalid Firebase storage URI protocol");
|
|
225
|
-
const key = u.pathname.slice(1);
|
|
226
|
-
if (!key) throw new Error("Invalid Firebase storage URI: missing key");
|
|
227
|
-
const [signedUrl] = await bucket.file(key).getSignedUrl({
|
|
228
|
-
action: "read",
|
|
229
|
-
expires: Date.now() + 3600 * 1e3
|
|
230
|
-
});
|
|
231
|
-
if (!signedUrl) throw new Error("Failed to generate download URL");
|
|
232
|
-
return { fileUrl: signedUrl };
|
|
182
|
+
const firebaseStorage = (0, __hot_updater_plugin_core.createStoragePlugin)({
|
|
183
|
+
name: "firebaseStorage",
|
|
184
|
+
supportedProtocol: "gs",
|
|
185
|
+
factory: (config) => {
|
|
186
|
+
let app;
|
|
187
|
+
try {
|
|
188
|
+
app = firebase_admin.app();
|
|
189
|
+
} catch {
|
|
190
|
+
app = firebase_admin.initializeApp(config);
|
|
233
191
|
}
|
|
234
|
-
|
|
235
|
-
|
|
192
|
+
const bucket = app.storage().bucket(config.storageBucket);
|
|
193
|
+
const getStorageKey = (0, __hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
|
|
194
|
+
return {
|
|
195
|
+
async delete(storageUri) {
|
|
196
|
+
const { bucket: bucketName, key } = (0, __hot_updater_plugin_core.parseStorageUri)(storageUri, "gs");
|
|
197
|
+
if (bucketName !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${bucketName}".`);
|
|
198
|
+
try {
|
|
199
|
+
const [files] = await bucket.getFiles({ prefix: key });
|
|
200
|
+
await Promise.all(files.map((file) => file.delete()));
|
|
201
|
+
} catch (e) {
|
|
202
|
+
console.error("Error listing or deleting files:", e);
|
|
203
|
+
throw new Error("Bundle Not Found");
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
async upload(key, filePath) {
|
|
207
|
+
try {
|
|
208
|
+
const fileContent = await fs_promises.default.readFile(filePath);
|
|
209
|
+
const contentType = (0, __hot_updater_plugin_core.getContentType)(filePath);
|
|
210
|
+
const storageKey = getStorageKey(key, path.default.basename(filePath));
|
|
211
|
+
await bucket.file(storageKey).save(fileContent, { metadata: { contentType } });
|
|
212
|
+
return { storageUri: `gs://${config.storageBucket}/${storageKey}` };
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error("Error uploading bundle:", error);
|
|
215
|
+
if (error instanceof Error) throw new Error(`Failed to upload bundle: ${error.message}`);
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
async getDownloadUrl(storageUri) {
|
|
220
|
+
const u = new URL(storageUri);
|
|
221
|
+
if (u.protocol.replace(":", "") !== "gs") throw new Error("Invalid Firebase storage URI protocol");
|
|
222
|
+
const key = u.pathname.slice(1);
|
|
223
|
+
if (!key) throw new Error("Invalid Firebase storage URI: missing key");
|
|
224
|
+
const [signedUrl] = await bucket.file(key).getSignedUrl({
|
|
225
|
+
action: "read",
|
|
226
|
+
expires: Date.now() + 3600 * 1e3
|
|
227
|
+
});
|
|
228
|
+
if (!signedUrl) throw new Error("Failed to generate download URL");
|
|
229
|
+
return { fileUrl: signedUrl };
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
});
|
|
236
234
|
|
|
237
235
|
//#endregion
|
|
238
236
|
exports.firebaseDatabase = firebaseDatabase;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import { DatabasePluginHooks, StoragePlugin, StoragePluginHooks } from "@hot-updater/plugin-core";
|
|
1
|
+
import * as _hot_updater_plugin_core1 from "@hot-updater/plugin-core";
|
|
3
2
|
import * as admin from "firebase-admin";
|
|
4
3
|
|
|
5
4
|
//#region src/firebaseDatabase.d.ts
|
|
6
|
-
declare const firebaseDatabase: (config: admin.AppOptions, hooks?: DatabasePluginHooks) => () =>
|
|
5
|
+
declare const firebaseDatabase: (config: admin.AppOptions, hooks?: _hot_updater_plugin_core1.DatabasePluginHooks) => (() => _hot_updater_plugin_core1.DatabasePlugin);
|
|
7
6
|
//#endregion
|
|
8
7
|
//#region src/firebaseStorage.d.ts
|
|
9
8
|
interface FirebaseStorageConfig extends admin.AppOptions {
|
|
@@ -13,6 +12,6 @@ interface FirebaseStorageConfig extends admin.AppOptions {
|
|
|
13
12
|
*/
|
|
14
13
|
basePath?: string;
|
|
15
14
|
}
|
|
16
|
-
declare const firebaseStorage: (config: FirebaseStorageConfig, hooks?: StoragePluginHooks) => () => StoragePlugin;
|
|
15
|
+
declare const firebaseStorage: (config: FirebaseStorageConfig, hooks?: _hot_updater_plugin_core1.StoragePluginHooks) => () => _hot_updater_plugin_core1.StoragePlugin;
|
|
17
16
|
//#endregion
|
|
18
17
|
export { FirebaseStorageConfig, firebaseDatabase, firebaseStorage };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import { DatabasePluginHooks, StoragePlugin, StoragePluginHooks } from "@hot-updater/plugin-core";
|
|
1
|
+
import * as _hot_updater_plugin_core1 from "@hot-updater/plugin-core";
|
|
3
2
|
import * as admin from "firebase-admin";
|
|
4
3
|
|
|
5
4
|
//#region src/firebaseDatabase.d.ts
|
|
6
|
-
declare const firebaseDatabase: (config: admin.AppOptions, hooks?: DatabasePluginHooks) => () =>
|
|
5
|
+
declare const firebaseDatabase: (config: admin.AppOptions, hooks?: _hot_updater_plugin_core1.DatabasePluginHooks) => (() => _hot_updater_plugin_core1.DatabasePlugin);
|
|
7
6
|
//#endregion
|
|
8
7
|
//#region src/firebaseStorage.d.ts
|
|
9
8
|
interface FirebaseStorageConfig extends admin.AppOptions {
|
|
@@ -13,6 +12,6 @@ interface FirebaseStorageConfig extends admin.AppOptions {
|
|
|
13
12
|
*/
|
|
14
13
|
basePath?: string;
|
|
15
14
|
}
|
|
16
|
-
declare const firebaseStorage: (config: FirebaseStorageConfig, hooks?: StoragePluginHooks) => () => StoragePlugin;
|
|
15
|
+
declare const firebaseStorage: (config: FirebaseStorageConfig, hooks?: _hot_updater_plugin_core1.StoragePluginHooks) => () => _hot_updater_plugin_core1.StoragePlugin;
|
|
17
16
|
//#endregion
|
|
18
17
|
export { FirebaseStorageConfig, firebaseDatabase, firebaseStorage };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { calculatePagination, createDatabasePlugin, createStorageKeyBuilder, getContentType, parseStorageUri } from "@hot-updater/plugin-core";
|
|
1
|
+
import { calculatePagination, createDatabasePlugin, createStorageKeyBuilder, createStoragePlugin, getContentType, parseStorageUri } from "@hot-updater/plugin-core";
|
|
2
2
|
import * as admin from "firebase-admin";
|
|
3
3
|
import fs from "fs/promises";
|
|
4
4
|
import path from "path";
|
|
@@ -18,194 +18,192 @@ const convertToBundle = (firestoreData) => ({
|
|
|
18
18
|
fingerprintHash: firestoreData.fingerprint_hash,
|
|
19
19
|
metadata: firestoreData?.metadata ?? {}
|
|
20
20
|
});
|
|
21
|
-
const firebaseDatabase = (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
for (const doc of querySnapshot.docs) {
|
|
67
|
-
const data = doc.data();
|
|
68
|
-
if (data.name) channels.add(data.name);
|
|
69
|
-
}
|
|
70
|
-
return Array.from(channels);
|
|
71
|
-
},
|
|
72
|
-
async commitBundle(context, { changedSets }) {
|
|
73
|
-
if (changedSets.length === 0) return;
|
|
74
|
-
let isTargetAppVersionChanged = false;
|
|
75
|
-
const deletedBundleIds = /* @__PURE__ */ new Set();
|
|
76
|
-
await context.db.runTransaction(async (transaction) => {
|
|
77
|
-
const bundlesSnapshot = await transaction.get(context.bundlesCollection);
|
|
78
|
-
const targetVersionsSnapshot = await transaction.get(context.db.collection("target_app_versions"));
|
|
79
|
-
const channelsSnapshot = await transaction.get(context.db.collection("channels"));
|
|
80
|
-
const bundlesMap = {};
|
|
81
|
-
for (const doc of bundlesSnapshot.docs) bundlesMap[doc.id] = doc.data();
|
|
82
|
-
for (const { operation, data } of changedSets) {
|
|
83
|
-
if (data.targetAppVersion) isTargetAppVersionChanged = true;
|
|
84
|
-
if (operation === "insert" || operation === "update") {
|
|
85
|
-
bundlesMap[data.id] = {
|
|
86
|
-
id: data.id,
|
|
87
|
-
channel: data.channel,
|
|
88
|
-
enabled: data.enabled,
|
|
89
|
-
should_force_update: data.shouldForceUpdate,
|
|
90
|
-
file_hash: data.fileHash,
|
|
91
|
-
git_commit_hash: data.gitCommitHash || null,
|
|
92
|
-
message: data.message || null,
|
|
93
|
-
platform: data.platform,
|
|
94
|
-
target_app_version: data.targetAppVersion,
|
|
95
|
-
storage_uri: data.storageUri,
|
|
96
|
-
fingerprint_hash: data.fingerprintHash,
|
|
97
|
-
metadata: data.metadata ?? {}
|
|
98
|
-
};
|
|
99
|
-
const channelRef = context.db.collection("channels").doc(data.channel);
|
|
100
|
-
transaction.set(channelRef, { name: data.channel }, { merge: true });
|
|
101
|
-
} else if (operation === "delete") {
|
|
102
|
-
if (!bundlesMap[data.id]) throw new Error(`Bundle with id ${data.id} not found`);
|
|
103
|
-
delete bundlesMap[data.id];
|
|
104
|
-
deletedBundleIds.add(data.id);
|
|
105
|
-
isTargetAppVersionChanged = true;
|
|
106
|
-
}
|
|
21
|
+
const firebaseDatabase = createDatabasePlugin({
|
|
22
|
+
name: "firebaseDatabase",
|
|
23
|
+
factory: (config) => {
|
|
24
|
+
let bundles = [];
|
|
25
|
+
let app;
|
|
26
|
+
try {
|
|
27
|
+
app = admin.app();
|
|
28
|
+
} catch {
|
|
29
|
+
app = admin.initializeApp(config);
|
|
30
|
+
}
|
|
31
|
+
const db = admin.firestore(app);
|
|
32
|
+
const bundlesCollection = db.collection("bundles");
|
|
33
|
+
return {
|
|
34
|
+
async getBundleById(bundleId) {
|
|
35
|
+
const found = bundles.find((b) => b.id === bundleId);
|
|
36
|
+
if (found) return found;
|
|
37
|
+
const bundleSnap = await bundlesCollection.doc(bundleId).get();
|
|
38
|
+
if (!bundleSnap.exists) return null;
|
|
39
|
+
return convertToBundle(bundleSnap.data());
|
|
40
|
+
},
|
|
41
|
+
async getBundles(options) {
|
|
42
|
+
const { where, limit, offset } = options;
|
|
43
|
+
let query = bundlesCollection;
|
|
44
|
+
if (where?.channel) query = query.where("channel", "==", where.channel);
|
|
45
|
+
if (where?.platform) query = query.where("platform", "==", where.platform);
|
|
46
|
+
query = query.orderBy("id", "desc");
|
|
47
|
+
const total = (await query.get()).size;
|
|
48
|
+
if (offset > 0) query = query.offset(offset);
|
|
49
|
+
if (limit) query = query.limit(limit);
|
|
50
|
+
bundles = (await query.get()).docs.map((doc) => convertToBundle(doc.data()));
|
|
51
|
+
return {
|
|
52
|
+
data: bundles,
|
|
53
|
+
pagination: calculatePagination(total, {
|
|
54
|
+
limit,
|
|
55
|
+
offset
|
|
56
|
+
})
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
async getChannels() {
|
|
60
|
+
const querySnapshot = await db.collection("channels").get();
|
|
61
|
+
if (querySnapshot.empty) return [];
|
|
62
|
+
const channels = /* @__PURE__ */ new Set();
|
|
63
|
+
for (const doc of querySnapshot.docs) {
|
|
64
|
+
const data = doc.data();
|
|
65
|
+
if (data.name) channels.add(data.name);
|
|
107
66
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
67
|
+
return Array.from(channels);
|
|
68
|
+
},
|
|
69
|
+
async commitBundle({ changedSets }) {
|
|
70
|
+
if (changedSets.length === 0) return;
|
|
71
|
+
let isTargetAppVersionChanged = false;
|
|
72
|
+
const deletedBundleIds = /* @__PURE__ */ new Set();
|
|
73
|
+
await db.runTransaction(async (transaction) => {
|
|
74
|
+
const bundlesSnapshot = await transaction.get(bundlesCollection);
|
|
75
|
+
const targetVersionsSnapshot = await transaction.get(db.collection("target_app_versions"));
|
|
76
|
+
const channelsSnapshot = await transaction.get(db.collection("channels"));
|
|
77
|
+
const bundlesMap = {};
|
|
78
|
+
for (const doc of bundlesSnapshot.docs) bundlesMap[doc.id] = doc.data();
|
|
79
|
+
for (const { operation, data } of changedSets) {
|
|
80
|
+
if (data.targetAppVersion) isTargetAppVersionChanged = true;
|
|
81
|
+
if (operation === "insert" || operation === "update") {
|
|
82
|
+
bundlesMap[data.id] = {
|
|
83
|
+
id: data.id,
|
|
84
|
+
channel: data.channel,
|
|
85
|
+
enabled: data.enabled,
|
|
86
|
+
should_force_update: data.shouldForceUpdate,
|
|
87
|
+
file_hash: data.fileHash,
|
|
88
|
+
git_commit_hash: data.gitCommitHash || null,
|
|
89
|
+
message: data.message || null,
|
|
90
|
+
platform: data.platform,
|
|
91
|
+
target_app_version: data.targetAppVersion,
|
|
92
|
+
storage_uri: data.storageUri,
|
|
93
|
+
fingerprint_hash: data.fingerprintHash,
|
|
94
|
+
metadata: data.metadata ?? {}
|
|
95
|
+
};
|
|
96
|
+
const channelRef = db.collection("channels").doc(data.channel);
|
|
97
|
+
transaction.set(channelRef, { name: data.channel }, { merge: true });
|
|
98
|
+
} else if (operation === "delete") {
|
|
99
|
+
if (!bundlesMap[data.id]) throw new Error(`Bundle with id ${data.id} not found`);
|
|
100
|
+
delete bundlesMap[data.id];
|
|
101
|
+
deletedBundleIds.add(data.id);
|
|
102
|
+
isTargetAppVersionChanged = true;
|
|
103
|
+
}
|
|
114
104
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
target_app_version: data.targetAppVersion || null,
|
|
130
|
-
storage_uri: data.storageUri,
|
|
131
|
-
fingerprint_hash: data.fingerprintHash,
|
|
132
|
-
metadata: data.metadata ?? {}
|
|
133
|
-
}, { merge: true });
|
|
134
|
-
if (data.targetAppVersion) {
|
|
135
|
-
const versionDocId = `${data.platform}_${data.channel}_${data.targetAppVersion}`;
|
|
136
|
-
const targetAppVersionsRef = context.db.collection("target_app_versions").doc(versionDocId);
|
|
137
|
-
transaction.set(targetAppVersionsRef, {
|
|
105
|
+
const requiredTargetVersionKeys = /* @__PURE__ */ new Set();
|
|
106
|
+
const requiredChannels = /* @__PURE__ */ new Set();
|
|
107
|
+
for (const bundle of Object.values(bundlesMap)) {
|
|
108
|
+
if (bundle.target_app_version) {
|
|
109
|
+
const key = `${bundle.platform}_${bundle.channel}_${bundle.target_app_version}`;
|
|
110
|
+
requiredTargetVersionKeys.add(key);
|
|
111
|
+
}
|
|
112
|
+
requiredChannels.add(bundle.channel);
|
|
113
|
+
}
|
|
114
|
+
for (const { operation, data } of changedSets) {
|
|
115
|
+
const bundleRef = bundlesCollection.doc(data.id);
|
|
116
|
+
if (operation === "insert" || operation === "update") {
|
|
117
|
+
transaction.set(bundleRef, {
|
|
118
|
+
id: data.id,
|
|
138
119
|
channel: data.channel,
|
|
120
|
+
enabled: data.enabled,
|
|
121
|
+
should_force_update: data.shouldForceUpdate,
|
|
122
|
+
file_hash: data.fileHash,
|
|
123
|
+
git_commit_hash: data.gitCommitHash || null,
|
|
124
|
+
message: data.message || null,
|
|
139
125
|
platform: data.platform,
|
|
140
|
-
target_app_version: data.targetAppVersion
|
|
126
|
+
target_app_version: data.targetAppVersion || null,
|
|
127
|
+
storage_uri: data.storageUri,
|
|
128
|
+
fingerprint_hash: data.fingerprintHash,
|
|
129
|
+
metadata: data.metadata ?? {}
|
|
141
130
|
}, { merge: true });
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
131
|
+
if (data.targetAppVersion) {
|
|
132
|
+
const versionDocId = `${data.platform}_${data.channel}_${data.targetAppVersion}`;
|
|
133
|
+
const targetAppVersionsRef = db.collection("target_app_versions").doc(versionDocId);
|
|
134
|
+
transaction.set(targetAppVersionsRef, {
|
|
135
|
+
channel: data.channel,
|
|
136
|
+
platform: data.platform,
|
|
137
|
+
target_app_version: data.targetAppVersion
|
|
138
|
+
}, { merge: true });
|
|
139
|
+
}
|
|
140
|
+
} else if (operation === "delete") transaction.delete(bundleRef);
|
|
141
|
+
}
|
|
142
|
+
if (isTargetAppVersionChanged) {
|
|
143
|
+
for (const targetDoc of targetVersionsSnapshot.docs) if (!requiredTargetVersionKeys.has(targetDoc.id)) transaction.delete(targetDoc.ref);
|
|
144
|
+
}
|
|
145
|
+
for (const channelDoc of channelsSnapshot.docs) if (!requiredChannels.has(channelDoc.id)) transaction.delete(channelDoc.ref);
|
|
146
|
+
});
|
|
147
|
+
for (const bundleId of deletedBundleIds) bundles = bundles.filter((b) => b.id !== bundleId);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
});
|
|
155
152
|
|
|
156
153
|
//#endregion
|
|
157
154
|
//#region src/firebaseStorage.ts
|
|
158
|
-
const firebaseStorage = (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
name: "firebaseStorage",
|
|
169
|
-
supportedProtocol: "gs",
|
|
170
|
-
async delete(storageUri) {
|
|
171
|
-
const { bucket: bucketName, key } = parseStorageUri(storageUri, "gs");
|
|
172
|
-
if (bucketName !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${bucketName}".`);
|
|
173
|
-
try {
|
|
174
|
-
const [files] = await bucket.getFiles({ prefix: key });
|
|
175
|
-
await Promise.all(files.map((file) => file.delete()));
|
|
176
|
-
} catch (e) {
|
|
177
|
-
console.error("Error listing or deleting files:", e);
|
|
178
|
-
throw new Error("Bundle Not Found");
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
async upload(key, filePath) {
|
|
182
|
-
try {
|
|
183
|
-
const fileContent = await fs.readFile(filePath);
|
|
184
|
-
const contentType = getContentType(filePath);
|
|
185
|
-
const storageKey = getStorageKey(key, path.basename(filePath));
|
|
186
|
-
await bucket.file(storageKey).save(fileContent, { metadata: { contentType } });
|
|
187
|
-
hooks?.onStorageUploaded?.();
|
|
188
|
-
return { storageUri: `gs://${config.storageBucket}/${storageKey}` };
|
|
189
|
-
} catch (error) {
|
|
190
|
-
console.error("Error uploading bundle:", error);
|
|
191
|
-
if (error instanceof Error) throw new Error(`Failed to upload bundle: ${error.message}`);
|
|
192
|
-
throw error;
|
|
193
|
-
}
|
|
194
|
-
},
|
|
195
|
-
async getDownloadUrl(storageUri) {
|
|
196
|
-
const u = new URL(storageUri);
|
|
197
|
-
if (u.protocol.replace(":", "") !== "gs") throw new Error("Invalid Firebase storage URI protocol");
|
|
198
|
-
const key = u.pathname.slice(1);
|
|
199
|
-
if (!key) throw new Error("Invalid Firebase storage URI: missing key");
|
|
200
|
-
const [signedUrl] = await bucket.file(key).getSignedUrl({
|
|
201
|
-
action: "read",
|
|
202
|
-
expires: Date.now() + 3600 * 1e3
|
|
203
|
-
});
|
|
204
|
-
if (!signedUrl) throw new Error("Failed to generate download URL");
|
|
205
|
-
return { fileUrl: signedUrl };
|
|
155
|
+
const firebaseStorage = createStoragePlugin({
|
|
156
|
+
name: "firebaseStorage",
|
|
157
|
+
supportedProtocol: "gs",
|
|
158
|
+
factory: (config) => {
|
|
159
|
+
let app;
|
|
160
|
+
try {
|
|
161
|
+
app = admin.app();
|
|
162
|
+
} catch {
|
|
163
|
+
app = admin.initializeApp(config);
|
|
206
164
|
}
|
|
207
|
-
|
|
208
|
-
|
|
165
|
+
const bucket = app.storage().bucket(config.storageBucket);
|
|
166
|
+
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
167
|
+
return {
|
|
168
|
+
async delete(storageUri) {
|
|
169
|
+
const { bucket: bucketName, key } = parseStorageUri(storageUri, "gs");
|
|
170
|
+
if (bucketName !== config.storageBucket) throw new Error(`Bucket name mismatch: expected "${config.storageBucket}", but found "${bucketName}".`);
|
|
171
|
+
try {
|
|
172
|
+
const [files] = await bucket.getFiles({ prefix: key });
|
|
173
|
+
await Promise.all(files.map((file) => file.delete()));
|
|
174
|
+
} catch (e) {
|
|
175
|
+
console.error("Error listing or deleting files:", e);
|
|
176
|
+
throw new Error("Bundle Not Found");
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
async upload(key, filePath) {
|
|
180
|
+
try {
|
|
181
|
+
const fileContent = await fs.readFile(filePath);
|
|
182
|
+
const contentType = getContentType(filePath);
|
|
183
|
+
const storageKey = getStorageKey(key, path.basename(filePath));
|
|
184
|
+
await bucket.file(storageKey).save(fileContent, { metadata: { contentType } });
|
|
185
|
+
return { storageUri: `gs://${config.storageBucket}/${storageKey}` };
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error("Error uploading bundle:", error);
|
|
188
|
+
if (error instanceof Error) throw new Error(`Failed to upload bundle: ${error.message}`);
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
async getDownloadUrl(storageUri) {
|
|
193
|
+
const u = new URL(storageUri);
|
|
194
|
+
if (u.protocol.replace(":", "") !== "gs") throw new Error("Invalid Firebase storage URI protocol");
|
|
195
|
+
const key = u.pathname.slice(1);
|
|
196
|
+
if (!key) throw new Error("Invalid Firebase storage URI: missing key");
|
|
197
|
+
const [signedUrl] = await bucket.file(key).getSignedUrl({
|
|
198
|
+
action: "read",
|
|
199
|
+
expires: Date.now() + 3600 * 1e3
|
|
200
|
+
});
|
|
201
|
+
if (!signedUrl) throw new Error("Failed to generate download URL");
|
|
202
|
+
return { fileUrl: signedUrl };
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
});
|
|
209
207
|
|
|
210
208
|
//#endregion
|
|
211
209
|
export { firebaseDatabase, firebaseStorage };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/firebase",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.21.
|
|
4
|
+
"version": "0.21.13",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"firebase": "^11.3.1",
|
|
38
|
-
"@hot-updater/
|
|
39
|
-
"@hot-updater/core": "0.21.
|
|
40
|
-
"@hot-updater/
|
|
38
|
+
"@hot-updater/cli-tools": "0.21.13",
|
|
39
|
+
"@hot-updater/plugin-core": "0.21.13",
|
|
40
|
+
"@hot-updater/core": "0.21.13"
|
|
41
41
|
},
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public"
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"fkill": "^9.0.0",
|
|
54
54
|
"hono": "^4.6.3",
|
|
55
55
|
"mime": "^4.0.4",
|
|
56
|
-
"@hot-updater/
|
|
57
|
-
"@hot-updater/
|
|
56
|
+
"@hot-updater/js": "0.21.13",
|
|
57
|
+
"@hot-updater/test-utils": "0.21.13"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"firebase-admin": "*"
|