@hot-updater/firebase 1.1.3 → 1.2.0-rc.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/dist/firebase/functions/index.cjs +920 -408
- package/dist/index.cjs +359 -12
- package/dist/index.d.cts +47 -2
- package/dist/index.d.mts +47 -2
- package/dist/index.mjs +359 -13
- package/dist/init/index.cjs +13 -4
- package/dist/init/index.mjs +13 -4
- package/package.json +8 -7
|
@@ -223,7 +223,7 @@ const bundleMetadataToRow = (metadata) => {
|
|
|
223
223
|
return metadata;
|
|
224
224
|
};
|
|
225
225
|
//#endregion
|
|
226
|
-
//#region ../../packages/protocol/dist/
|
|
226
|
+
//#region ../../packages/protocol/dist/uuid-kZvLA0Ly.mjs
|
|
227
227
|
const LETTER_DASH_NUMBER$2 = "[a-zA-Z0-9-]";
|
|
228
228
|
const NUMERIC_IDENTIFIER$2 = String.raw`0|[1-9]\d*`;
|
|
229
229
|
const NUMERIC_IDENTIFIER_LOOSE$2 = String.raw`\d+`;
|
|
@@ -336,128 +336,6 @@ function canonicalizeAppVersion(appVersion) {
|
|
|
336
336
|
const version = coerce$1(appVersion);
|
|
337
337
|
return version ? normalizeFull(version) : null;
|
|
338
338
|
}
|
|
339
|
-
const getManifestStorageUri = (bundle) => bundle.manifestStorageUri;
|
|
340
|
-
const getManifestFileHash = (bundle) => bundle.manifestFileHash;
|
|
341
|
-
const getAssetBaseStorageUri = (bundle) => bundle.assetBaseStorageUri;
|
|
342
|
-
const isBundlePatchArtifact = (value) => {
|
|
343
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
344
|
-
const candidate = value;
|
|
345
|
-
return typeof candidate.baseBundleId === "string" && typeof candidate.baseFileHash === "string" && typeof candidate.patchFileHash === "string" && typeof candidate.patchStorageUri === "string";
|
|
346
|
-
};
|
|
347
|
-
const readBundlePatchArray = (patches) => {
|
|
348
|
-
if (!Array.isArray(patches)) return [];
|
|
349
|
-
return patches.filter(isBundlePatchArtifact);
|
|
350
|
-
};
|
|
351
|
-
const getBundlePatches = (bundle) => {
|
|
352
|
-
const patches = readBundlePatchArray(bundle.patches);
|
|
353
|
-
const seenBaseBundleIds = /* @__PURE__ */ new Set();
|
|
354
|
-
return patches.filter((patch) => {
|
|
355
|
-
if (seenBaseBundleIds.has(patch.baseBundleId)) return false;
|
|
356
|
-
seenBaseBundleIds.add(patch.baseBundleId);
|
|
357
|
-
return true;
|
|
358
|
-
});
|
|
359
|
-
};
|
|
360
|
-
const getBundlePatch = (bundle, baseBundleId) => {
|
|
361
|
-
return getBundlePatches(bundle).find((patch) => patch.baseBundleId === baseBundleId) ?? null;
|
|
362
|
-
};
|
|
363
|
-
const NUMERIC_COHORT_SIZE = 1e3;
|
|
364
|
-
const DEFAULT_ROLLOUT_COHORT_COUNT = NUMERIC_COHORT_SIZE;
|
|
365
|
-
const CUSTOM_COHORT_PATTERN = /^[a-z0-9-]+$/;
|
|
366
|
-
function parseNumericCohortValue(cohort) {
|
|
367
|
-
if (!/^\d+$/.test(cohort)) return null;
|
|
368
|
-
const parsed = Number.parseInt(cohort, 10);
|
|
369
|
-
if (Number.isNaN(parsed) || parsed < 1 || parsed > 1e3) return null;
|
|
370
|
-
return parsed;
|
|
371
|
-
}
|
|
372
|
-
function positiveMod(value, modulus) {
|
|
373
|
-
return (value % modulus + modulus) % modulus;
|
|
374
|
-
}
|
|
375
|
-
function hashString(value) {
|
|
376
|
-
let hash = 0;
|
|
377
|
-
for (let i = 0; i < value.length; i++) {
|
|
378
|
-
const char = value.charCodeAt(i);
|
|
379
|
-
hash = (hash << 5) - hash + char;
|
|
380
|
-
hash |= 0;
|
|
381
|
-
}
|
|
382
|
-
return hash;
|
|
383
|
-
}
|
|
384
|
-
function gcd(a, b) {
|
|
385
|
-
let x = Math.abs(a);
|
|
386
|
-
let y = Math.abs(b);
|
|
387
|
-
while (y !== 0) {
|
|
388
|
-
const next = x % y;
|
|
389
|
-
x = y;
|
|
390
|
-
y = next;
|
|
391
|
-
}
|
|
392
|
-
return x;
|
|
393
|
-
}
|
|
394
|
-
function modularInverse(value, modulus) {
|
|
395
|
-
let t = 0;
|
|
396
|
-
let newT = 1;
|
|
397
|
-
let r = modulus;
|
|
398
|
-
let newR = positiveMod(value, modulus);
|
|
399
|
-
while (newR !== 0) {
|
|
400
|
-
const quotient = Math.floor(r / newR);
|
|
401
|
-
[t, newT] = [newT, t - quotient * newT];
|
|
402
|
-
[r, newR] = [newR, r - quotient * newR];
|
|
403
|
-
}
|
|
404
|
-
if (r > 1) throw new Error(`No modular inverse for ${value} mod ${modulus}`);
|
|
405
|
-
return positiveMod(t, modulus);
|
|
406
|
-
}
|
|
407
|
-
function getRolloutShuffleParameters(bundleId) {
|
|
408
|
-
let multiplier = positiveMod(hashString(`${bundleId}:multiplier`), 997);
|
|
409
|
-
if (multiplier === 0) multiplier = 1;
|
|
410
|
-
while (gcd(multiplier, NUMERIC_COHORT_SIZE) !== 1) {
|
|
411
|
-
multiplier = positiveMod(multiplier + 1, NUMERIC_COHORT_SIZE);
|
|
412
|
-
if (multiplier === 0) multiplier = 1;
|
|
413
|
-
}
|
|
414
|
-
const offset = positiveMod(hashString(`${bundleId}:offset`), NUMERIC_COHORT_SIZE);
|
|
415
|
-
return {
|
|
416
|
-
multiplier,
|
|
417
|
-
offset,
|
|
418
|
-
inverseMultiplier: modularInverse(multiplier, NUMERIC_COHORT_SIZE)
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
function normalizeRolloutCohortCount(rolloutCohortCount) {
|
|
422
|
-
if (rolloutCohortCount === null || rolloutCohortCount === void 0) return DEFAULT_ROLLOUT_COHORT_COUNT;
|
|
423
|
-
if (rolloutCohortCount <= 0) return 0;
|
|
424
|
-
if (rolloutCohortCount >= 1e3) return NUMERIC_COHORT_SIZE;
|
|
425
|
-
return Math.floor(rolloutCohortCount);
|
|
426
|
-
}
|
|
427
|
-
function normalizeCohortValue(cohort) {
|
|
428
|
-
const normalized = cohort.trim().toLowerCase();
|
|
429
|
-
const numericCohort = parseNumericCohortValue(normalized);
|
|
430
|
-
if (numericCohort !== null) return String(numericCohort);
|
|
431
|
-
return normalized;
|
|
432
|
-
}
|
|
433
|
-
function getNumericCohortValue(cohort) {
|
|
434
|
-
return parseNumericCohortValue(normalizeCohortValue(cohort));
|
|
435
|
-
}
|
|
436
|
-
function isNumericCohort(cohort) {
|
|
437
|
-
return getNumericCohortValue(cohort) !== null;
|
|
438
|
-
}
|
|
439
|
-
function isCustomCohort(cohort) {
|
|
440
|
-
const normalized = normalizeCohortValue(cohort);
|
|
441
|
-
return normalized.length > 0 && normalized.length <= 64 && !/^\d+$/.test(normalized) && CUSTOM_COHORT_PATTERN.test(normalized);
|
|
442
|
-
}
|
|
443
|
-
function isValidCohort(cohort) {
|
|
444
|
-
const normalized = normalizeCohortValue(cohort);
|
|
445
|
-
return isNumericCohort(normalized) || isCustomCohort(normalized);
|
|
446
|
-
}
|
|
447
|
-
function getNumericCohortRolloutPosition(bundleId, cohortValue) {
|
|
448
|
-
if (cohortValue < 1 || cohortValue > 1e3) throw new Error(`Invalid numeric cohort: ${cohortValue}`);
|
|
449
|
-
const { offset, inverseMultiplier } = getRolloutShuffleParameters(bundleId);
|
|
450
|
-
return positiveMod(inverseMultiplier * (cohortValue - 1 - offset), NUMERIC_COHORT_SIZE);
|
|
451
|
-
}
|
|
452
|
-
function getRolledOutNumericCohorts(bundleId, rolloutCohortCount) {
|
|
453
|
-
const normalizedRolloutCount = normalizeRolloutCohortCount(rolloutCohortCount);
|
|
454
|
-
if (normalizedRolloutCount <= 0) return [];
|
|
455
|
-
if (normalizedRolloutCount >= 1e3) return Array.from({ length: NUMERIC_COHORT_SIZE }, (_, index) => index + 1);
|
|
456
|
-
const { multiplier, offset } = getRolloutShuffleParameters(bundleId);
|
|
457
|
-
return Array.from({ length: normalizedRolloutCount }, (_, position) => positiveMod(multiplier * position + offset, NUMERIC_COHORT_SIZE)).map((zeroBasedCohort) => zeroBasedCohort + 1).sort((left, right) => left - right);
|
|
458
|
-
}
|
|
459
|
-
const RELEASE_CATALOG_FALLBACK_POLICY = "BUILTIN_IF_ACTIVE_INELIGIBLE";
|
|
460
|
-
const MAX_COMPILED_CATALOG_BYTES = 262144;
|
|
461
339
|
const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
462
340
|
const MAX_CATALOG_SEGMENT_LENGTH = 255;
|
|
463
341
|
function assertCatalogSegment(value, name) {
|
|
@@ -582,6 +460,130 @@ const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
|
582
460
|
const UUID_V7_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
583
461
|
const isUUIDv7 = (value) => typeof value === "string" && UUID_V7_PATTERN.test(value);
|
|
584
462
|
//#endregion
|
|
463
|
+
//#region ../../packages/protocol/dist/index.mjs
|
|
464
|
+
const getManifestStorageUri = (bundle) => bundle.manifestStorageUri;
|
|
465
|
+
const getManifestFileHash = (bundle) => bundle.manifestFileHash;
|
|
466
|
+
const getAssetBaseStorageUri = (bundle) => bundle.assetBaseStorageUri;
|
|
467
|
+
const isBundlePatchArtifact = (value) => {
|
|
468
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
469
|
+
const candidate = value;
|
|
470
|
+
return typeof candidate.baseBundleId === "string" && typeof candidate.baseFileHash === "string" && typeof candidate.patchFileHash === "string" && typeof candidate.patchStorageUri === "string";
|
|
471
|
+
};
|
|
472
|
+
const readBundlePatchArray = (patches) => {
|
|
473
|
+
if (!Array.isArray(patches)) return [];
|
|
474
|
+
return patches.filter(isBundlePatchArtifact);
|
|
475
|
+
};
|
|
476
|
+
const getBundlePatches = (bundle) => {
|
|
477
|
+
const patches = readBundlePatchArray(bundle.patches);
|
|
478
|
+
const seenBaseBundleIds = /* @__PURE__ */ new Set();
|
|
479
|
+
return patches.filter((patch) => {
|
|
480
|
+
if (seenBaseBundleIds.has(patch.baseBundleId)) return false;
|
|
481
|
+
seenBaseBundleIds.add(patch.baseBundleId);
|
|
482
|
+
return true;
|
|
483
|
+
});
|
|
484
|
+
};
|
|
485
|
+
const getBundlePatch = (bundle, baseBundleId) => {
|
|
486
|
+
return getBundlePatches(bundle).find((patch) => patch.baseBundleId === baseBundleId) ?? null;
|
|
487
|
+
};
|
|
488
|
+
const NUMERIC_COHORT_SIZE = 1e3;
|
|
489
|
+
const DEFAULT_ROLLOUT_COHORT_COUNT = NUMERIC_COHORT_SIZE;
|
|
490
|
+
const CUSTOM_COHORT_PATTERN = /^[a-z0-9-]+$/;
|
|
491
|
+
function parseNumericCohortValue(cohort) {
|
|
492
|
+
if (!/^\d+$/.test(cohort)) return null;
|
|
493
|
+
const parsed = Number.parseInt(cohort, 10);
|
|
494
|
+
if (Number.isNaN(parsed) || parsed < 1 || parsed > 1e3) return null;
|
|
495
|
+
return parsed;
|
|
496
|
+
}
|
|
497
|
+
function positiveMod(value, modulus) {
|
|
498
|
+
return (value % modulus + modulus) % modulus;
|
|
499
|
+
}
|
|
500
|
+
function hashString(value) {
|
|
501
|
+
let hash = 0;
|
|
502
|
+
for (let i = 0; i < value.length; i++) {
|
|
503
|
+
const char = value.charCodeAt(i);
|
|
504
|
+
hash = (hash << 5) - hash + char;
|
|
505
|
+
hash |= 0;
|
|
506
|
+
}
|
|
507
|
+
return hash;
|
|
508
|
+
}
|
|
509
|
+
function gcd(a, b) {
|
|
510
|
+
let x = Math.abs(a);
|
|
511
|
+
let y = Math.abs(b);
|
|
512
|
+
while (y !== 0) {
|
|
513
|
+
const next = x % y;
|
|
514
|
+
x = y;
|
|
515
|
+
y = next;
|
|
516
|
+
}
|
|
517
|
+
return x;
|
|
518
|
+
}
|
|
519
|
+
function modularInverse(value, modulus) {
|
|
520
|
+
let t = 0;
|
|
521
|
+
let newT = 1;
|
|
522
|
+
let r = modulus;
|
|
523
|
+
let newR = positiveMod(value, modulus);
|
|
524
|
+
while (newR !== 0) {
|
|
525
|
+
const quotient = Math.floor(r / newR);
|
|
526
|
+
[t, newT] = [newT, t - quotient * newT];
|
|
527
|
+
[r, newR] = [newR, r - quotient * newR];
|
|
528
|
+
}
|
|
529
|
+
if (r > 1) throw new Error(`No modular inverse for ${value} mod ${modulus}`);
|
|
530
|
+
return positiveMod(t, modulus);
|
|
531
|
+
}
|
|
532
|
+
function getRolloutShuffleParameters(bundleId) {
|
|
533
|
+
let multiplier = positiveMod(hashString(`${bundleId}:multiplier`), 997);
|
|
534
|
+
if (multiplier === 0) multiplier = 1;
|
|
535
|
+
while (gcd(multiplier, NUMERIC_COHORT_SIZE) !== 1) {
|
|
536
|
+
multiplier = positiveMod(multiplier + 1, NUMERIC_COHORT_SIZE);
|
|
537
|
+
if (multiplier === 0) multiplier = 1;
|
|
538
|
+
}
|
|
539
|
+
const offset = positiveMod(hashString(`${bundleId}:offset`), NUMERIC_COHORT_SIZE);
|
|
540
|
+
return {
|
|
541
|
+
multiplier,
|
|
542
|
+
offset,
|
|
543
|
+
inverseMultiplier: modularInverse(multiplier, NUMERIC_COHORT_SIZE)
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function normalizeRolloutCohortCount(rolloutCohortCount) {
|
|
547
|
+
if (rolloutCohortCount === null || rolloutCohortCount === void 0) return DEFAULT_ROLLOUT_COHORT_COUNT;
|
|
548
|
+
if (rolloutCohortCount <= 0) return 0;
|
|
549
|
+
if (rolloutCohortCount >= 1e3) return NUMERIC_COHORT_SIZE;
|
|
550
|
+
return Math.floor(rolloutCohortCount);
|
|
551
|
+
}
|
|
552
|
+
function normalizeCohortValue(cohort) {
|
|
553
|
+
const normalized = cohort.trim().toLowerCase();
|
|
554
|
+
const numericCohort = parseNumericCohortValue(normalized);
|
|
555
|
+
if (numericCohort !== null) return String(numericCohort);
|
|
556
|
+
return normalized;
|
|
557
|
+
}
|
|
558
|
+
function getNumericCohortValue(cohort) {
|
|
559
|
+
return parseNumericCohortValue(normalizeCohortValue(cohort));
|
|
560
|
+
}
|
|
561
|
+
function isNumericCohort(cohort) {
|
|
562
|
+
return getNumericCohortValue(cohort) !== null;
|
|
563
|
+
}
|
|
564
|
+
function isCustomCohort(cohort) {
|
|
565
|
+
const normalized = normalizeCohortValue(cohort);
|
|
566
|
+
return normalized.length > 0 && normalized.length <= 64 && !/^\d+$/.test(normalized) && CUSTOM_COHORT_PATTERN.test(normalized);
|
|
567
|
+
}
|
|
568
|
+
function isValidCohort(cohort) {
|
|
569
|
+
const normalized = normalizeCohortValue(cohort);
|
|
570
|
+
return isNumericCohort(normalized) || isCustomCohort(normalized);
|
|
571
|
+
}
|
|
572
|
+
function getNumericCohortRolloutPosition(bundleId, cohortValue) {
|
|
573
|
+
if (cohortValue < 1 || cohortValue > 1e3) throw new Error(`Invalid numeric cohort: ${cohortValue}`);
|
|
574
|
+
const { offset, inverseMultiplier } = getRolloutShuffleParameters(bundleId);
|
|
575
|
+
return positiveMod(inverseMultiplier * (cohortValue - 1 - offset), NUMERIC_COHORT_SIZE);
|
|
576
|
+
}
|
|
577
|
+
function getRolledOutNumericCohorts(bundleId, rolloutCohortCount) {
|
|
578
|
+
const normalizedRolloutCount = normalizeRolloutCohortCount(rolloutCohortCount);
|
|
579
|
+
if (normalizedRolloutCount <= 0) return [];
|
|
580
|
+
if (normalizedRolloutCount >= 1e3) return Array.from({ length: NUMERIC_COHORT_SIZE }, (_, index) => index + 1);
|
|
581
|
+
const { multiplier, offset } = getRolloutShuffleParameters(bundleId);
|
|
582
|
+
return Array.from({ length: normalizedRolloutCount }, (_, position) => positiveMod(multiplier * position + offset, NUMERIC_COHORT_SIZE)).map((zeroBasedCohort) => zeroBasedCohort + 1).sort((left, right) => left - right);
|
|
583
|
+
}
|
|
584
|
+
const RELEASE_CATALOG_FALLBACK_POLICY = "BUILTIN_IF_ACTIVE_INELIGIBLE";
|
|
585
|
+
const MAX_COMPILED_CATALOG_BYTES = 262144;
|
|
586
|
+
//#endregion
|
|
585
587
|
//#region ../plugin-core/dist/databaseRows.mjs
|
|
586
588
|
const releaseRowToRelease = (row) => ({
|
|
587
589
|
id: row.id,
|
|
@@ -1901,12 +1903,17 @@ const SETTINGS_TABLE = {
|
|
|
1901
1903
|
const ENGINE_SCHEMA_KEY = "schema.engine";
|
|
1902
1904
|
const CORE_SCHEMA_KEY = "schema.core";
|
|
1903
1905
|
const stored = ({ key, expected, found }) => `"${key}" is ${found === null ? "missing" : `"${found}"`}; expected "${expected}"`;
|
|
1904
|
-
|
|
1906
|
+
/**
|
|
1907
|
+
* How to migrate a database the fence refused: doctor names the command for
|
|
1908
|
+
* it, init again for a deployment init manages, or the database's own
|
|
1909
|
+
* tooling in the server's project.
|
|
1910
|
+
*/
|
|
1911
|
+
const MIGRATE_SCHEMA_FIX = "Run `npx hot-updater doctor` in the project whose hot-updater.config.ts names this database: it names the command that migrates it. In a server project, `hot-updater db migrate` migrates the database its server file names.";
|
|
1912
|
+
const settingMessage = (adapterName, { key, expected, found }) => `Hot Updater schema setting "${key}" for ${adapterName} is ${found === null ? "missing" : `"${found}"`}; expected "${expected}". ${MIGRATE_SCHEMA_FIX}`;
|
|
1905
1913
|
/** The plugin a `schema.<id>` setting belongs to; undefined for core's and the engine's. */
|
|
1906
1914
|
const pluginOf = ({ key }) => key.startsWith("schema.") && key !== "schema.engine" && key !== CORE_SCHEMA_KEY ? key.slice(7) : void 0;
|
|
1907
1915
|
const listed = (items) => items.length < 2 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
|
|
1908
|
-
|
|
1909
|
-
const PLUGIN_TABLES_FIX = "Run `hot-updater db migrate`. A managed server runs only its provider's plugins, and rerunning `hot-updater init --provider <provider>` applies the provider's migrations. Run `hot-updater db` commands in the server's project: they load the server file named on the command line, or src/hotUpdater.* or src/db.*, and don't read hot-updater.config.ts.";
|
|
1916
|
+
const settingsMessage = (adapterName, settings) => `Hot Updater schema settings for ${adapterName} are not migrated: ${settings.map(stored).join(", and ")}. ${MIGRATE_SCHEMA_FIX}`;
|
|
1910
1917
|
const pluginsMessage = (adapterName, plugins, settings, fix) => `The tables of ${plugins.length === 1 ? "plugin" : "plugins"} ${listed(plugins.map((id) => `"${id}"`))} are not migrated on ${adapterName}: schema ${settings.length === 1 ? "setting" : "settings"} ${settings.map(stored).join(", and ")}. ${fix}`;
|
|
1911
1918
|
/** The database needs `hot-updater db migrate`, or a new database; the handler answers 503. */
|
|
1912
1919
|
var HotUpdaterSchemaMigrationRequiredError = class extends Error {
|
|
@@ -1923,7 +1930,7 @@ var HotUpdaterSchemaMigrationRequiredError = class extends Error {
|
|
|
1923
1930
|
constructor(adapterName, currentVersion, setting, options) {
|
|
1924
1931
|
const settings = options?.settings ?? (setting === void 0 ? [] : [setting]);
|
|
1925
1932
|
const plugins = settings.flatMap((mismatch) => pluginOf(mismatch) ?? []);
|
|
1926
|
-
super(setting === void 0 ? currentVersion === void 0 ? `Hot Updater database schema is not initialized for ${adapterName}.
|
|
1933
|
+
super(setting === void 0 ? currentVersion === void 0 ? `Hot Updater database schema is not initialized for ${adapterName}. ${MIGRATE_SCHEMA_FIX}` : `Hot Updater v1 cannot migrate schema ${currentVersion} in place. Create a new empty database and run \`hot-updater db migrate\`.` : plugins.length > 0 && plugins.length === settings.length ? pluginsMessage(adapterName, plugins, settings, options?.fix ?? MIGRATE_SCHEMA_FIX) : settings.length > 1 && !(setting.key === "schema.engine" && setting.found === null) ? settingsMessage(adapterName, settings) : settingMessage(adapterName, setting), options?.cause === void 0 ? void 0 : { cause: options.cause });
|
|
1927
1934
|
this.adapterName = adapterName;
|
|
1928
1935
|
this.currentVersion = currentVersion;
|
|
1929
1936
|
this.setting = setting;
|
|
@@ -1987,17 +1994,37 @@ const checkSchemaFence = async (adapter, adapterName, expected, fix) => {
|
|
|
1987
1994
|
};
|
|
1988
1995
|
/** The database name a fenced adapter carries, so a server can fence its plugins' rows too. */
|
|
1989
1996
|
const FENCED = Symbol.for("@hot-updater/server/schema-fence");
|
|
1997
|
+
/**
|
|
1998
|
+
* The adapter a fence reads its settings through and the settings it
|
|
1999
|
+
* expects, so a fence over it checks them with its own in one read. Another
|
|
2000
|
+
* key than `FENCED`, whose value another copy of plugin-core reads as the
|
|
2001
|
+
* name.
|
|
2002
|
+
*/
|
|
2003
|
+
const FENCED_SETTINGS = Symbol.for("@hot-updater/server/schema-fence-settings");
|
|
1990
2004
|
/** The database name an adapter behind the schema fence reports under. */
|
|
1991
2005
|
const fencedName = (adapter) => adapter[FENCED];
|
|
1992
2006
|
/**
|
|
1993
2007
|
* The adapter behind the schema fence: a process's first read or write
|
|
1994
2008
|
* checks the settings first. Success is kept; a failure is checked again on
|
|
1995
|
-
* the next call.
|
|
2009
|
+
* the next call. A fence over another one, as a server's plugins' over
|
|
2010
|
+
* core's, checks the inner fence's settings with its own in that one read,
|
|
2011
|
+
* so its refusal lists every setting missing or different.
|
|
1996
2012
|
*/
|
|
1997
2013
|
const withSchemaFence = (adapter, adapterName, expected, fix) => {
|
|
2014
|
+
const inner = adapter[FENCED_SETTINGS];
|
|
2015
|
+
const settings = inner === void 0 ? {
|
|
2016
|
+
adapter,
|
|
2017
|
+
expected
|
|
2018
|
+
} : {
|
|
2019
|
+
adapter: inner.adapter,
|
|
2020
|
+
expected: {
|
|
2021
|
+
...inner.expected,
|
|
2022
|
+
...expected
|
|
2023
|
+
}
|
|
2024
|
+
};
|
|
1998
2025
|
let ready;
|
|
1999
2026
|
const fence = () => {
|
|
2000
|
-
ready ??= checkSchemaFence(adapter, adapterName, expected, fix).catch((error) => {
|
|
2027
|
+
ready ??= checkSchemaFence(settings.adapter, adapterName, settings.expected, fix).catch((error) => {
|
|
2001
2028
|
ready = void 0;
|
|
2002
2029
|
throw error;
|
|
2003
2030
|
});
|
|
@@ -2006,6 +2033,7 @@ const withSchemaFence = (adapter, adapterName, expected, fix) => {
|
|
|
2006
2033
|
return {
|
|
2007
2034
|
...adapter,
|
|
2008
2035
|
[FENCED]: adapterName,
|
|
2036
|
+
[FENCED_SETTINGS]: settings,
|
|
2009
2037
|
get: (table, keys) => fence().then(() => adapter.get(table, keys)),
|
|
2010
2038
|
query: (table, request) => fence().then(() => adapter.query(table, request)),
|
|
2011
2039
|
write: (ops) => fence().then(() => adapter.write(ops)),
|
|
@@ -2064,259 +2092,388 @@ const CHANNEL_ID = {
|
|
|
2064
2092
|
maxLength: 1408,
|
|
2065
2093
|
ascii: true
|
|
2066
2094
|
};
|
|
2067
|
-
/**
|
|
2068
|
-
|
|
2095
|
+
/**
|
|
2096
|
+
* Core's schema version: the `schema.core` settings row. Bump it when core
|
|
2097
|
+
* adds a table, then regenerate the managed migrations with
|
|
2098
|
+
* HOT_UPDATER_UPDATE_SQL=1 (the D1 and Supabase schema specs), so the next
|
|
2099
|
+
* release's numbered migration carries the change and `hot-updater init`
|
|
2100
|
+
* applies it to managed databases an earlier release created. The official
|
|
2101
|
+
* plugins' `schemaVersion`s follow the same rule. A bump only adds tables:
|
|
2102
|
+
* every database's migrator creates missing tables and then records the
|
|
2103
|
+
* version, and none of them changes an existing table, so changing one
|
|
2104
|
+
* needs a migration step of its own first. The schema fence answers 503 on
|
|
2105
|
+
* a database whose row differs, until it runs.
|
|
2106
|
+
*/
|
|
2107
|
+
const HOT_UPDATER_SCHEMA_VERSION = "1.2.0";
|
|
2108
|
+
const bundles = defineTable({
|
|
2109
|
+
id: {
|
|
2110
|
+
type: "string",
|
|
2111
|
+
maxLength: 36
|
|
2112
|
+
},
|
|
2113
|
+
platform: {
|
|
2114
|
+
type: "string",
|
|
2115
|
+
maxLength: 16
|
|
2116
|
+
},
|
|
2117
|
+
git_commit_hash: {
|
|
2118
|
+
type: "string",
|
|
2119
|
+
maxLength: 255,
|
|
2120
|
+
required: false
|
|
2121
|
+
},
|
|
2122
|
+
metadata: { type: "json" },
|
|
2123
|
+
manifest_storage_uri: { type: "string" },
|
|
2124
|
+
manifest_file_hash: { type: "string" },
|
|
2125
|
+
asset_base_storage_uri: { type: "string" }
|
|
2126
|
+
}, {
|
|
2127
|
+
key: ["id"],
|
|
2128
|
+
indexes: {
|
|
2129
|
+
byPlatform: {
|
|
2130
|
+
eq: ["platform"],
|
|
2131
|
+
sort: ["id"]
|
|
2132
|
+
},
|
|
2133
|
+
all: {
|
|
2134
|
+
eq: [],
|
|
2135
|
+
sort: ["id"]
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
});
|
|
2139
|
+
const bundlePatches = defineTable({
|
|
2140
|
+
id: {
|
|
2141
|
+
type: "string",
|
|
2142
|
+
maxLength: 255
|
|
2143
|
+
},
|
|
2144
|
+
bundle_id: {
|
|
2145
|
+
type: "string",
|
|
2146
|
+
maxLength: 36,
|
|
2147
|
+
references: {
|
|
2148
|
+
model: "bundles",
|
|
2149
|
+
onDelete: "cascade"
|
|
2150
|
+
}
|
|
2151
|
+
},
|
|
2152
|
+
base_bundle_id: {
|
|
2153
|
+
type: "string",
|
|
2154
|
+
maxLength: 36,
|
|
2155
|
+
references: {
|
|
2156
|
+
model: "bundles",
|
|
2157
|
+
onDelete: "cascade"
|
|
2158
|
+
}
|
|
2159
|
+
},
|
|
2160
|
+
base_file_hash: { type: "string" },
|
|
2161
|
+
patch_file_hash: { type: "string" },
|
|
2162
|
+
patch_storage_uri: { type: "string" },
|
|
2163
|
+
byte_size: { type: "integer" },
|
|
2164
|
+
order_index: { type: "integer" }
|
|
2165
|
+
}, {
|
|
2166
|
+
key: ["id"],
|
|
2167
|
+
indexes: {
|
|
2168
|
+
pair: {
|
|
2169
|
+
eq: ["bundle_id", "base_bundle_id"],
|
|
2170
|
+
sort: [],
|
|
2171
|
+
unique: true
|
|
2172
|
+
},
|
|
2173
|
+
byBundle: {
|
|
2174
|
+
eq: ["bundle_id"],
|
|
2175
|
+
sort: ["order_index"],
|
|
2176
|
+
root: { model: "bundles" }
|
|
2177
|
+
},
|
|
2178
|
+
byBase: {
|
|
2179
|
+
eq: ["base_bundle_id"],
|
|
2180
|
+
sort: ["bundle_id"]
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
});
|
|
2184
|
+
const releases = defineTable({
|
|
2185
|
+
id: {
|
|
2186
|
+
type: "string",
|
|
2187
|
+
maxLength: 36
|
|
2188
|
+
},
|
|
2189
|
+
revision: { type: "integer" },
|
|
2190
|
+
scope_key: {
|
|
2191
|
+
type: "string",
|
|
2192
|
+
maxLength: 2048,
|
|
2193
|
+
ascii: true
|
|
2194
|
+
},
|
|
2195
|
+
channel_id: {
|
|
2196
|
+
...CHANNEL_ID,
|
|
2197
|
+
references: {
|
|
2198
|
+
model: "channels",
|
|
2199
|
+
onDelete: "restrict"
|
|
2200
|
+
}
|
|
2201
|
+
},
|
|
2202
|
+
platform: {
|
|
2203
|
+
type: "string",
|
|
2204
|
+
maxLength: 16
|
|
2205
|
+
},
|
|
2206
|
+
kind: {
|
|
2207
|
+
type: "string",
|
|
2208
|
+
maxLength: 16
|
|
2209
|
+
},
|
|
2210
|
+
bundle_id: {
|
|
2211
|
+
type: "string",
|
|
2212
|
+
maxLength: 36,
|
|
2213
|
+
required: false,
|
|
2214
|
+
references: {
|
|
2215
|
+
model: "bundles",
|
|
2216
|
+
onDelete: "restrict"
|
|
2217
|
+
}
|
|
2218
|
+
},
|
|
2219
|
+
strategy: {
|
|
2220
|
+
type: "string",
|
|
2221
|
+
maxLength: 16
|
|
2222
|
+
},
|
|
2223
|
+
target_app_version: {
|
|
2224
|
+
type: "string",
|
|
2225
|
+
required: false
|
|
2226
|
+
},
|
|
2227
|
+
fingerprint_hash: {
|
|
2228
|
+
type: "string",
|
|
2229
|
+
maxLength: 255,
|
|
2230
|
+
required: false
|
|
2231
|
+
},
|
|
2232
|
+
enabled: { type: "boolean" },
|
|
2233
|
+
should_force_update: { type: "boolean" },
|
|
2234
|
+
message: {
|
|
2235
|
+
type: "string",
|
|
2236
|
+
required: false
|
|
2237
|
+
},
|
|
2238
|
+
rollout_cohort_count: { type: "integer" },
|
|
2239
|
+
target_cohorts: { type: "json" },
|
|
2240
|
+
operation: {
|
|
2241
|
+
type: "string",
|
|
2242
|
+
maxLength: 16
|
|
2243
|
+
},
|
|
2244
|
+
source_release_id: {
|
|
2245
|
+
type: "string",
|
|
2246
|
+
maxLength: 36,
|
|
2247
|
+
required: false,
|
|
2248
|
+
references: {
|
|
2249
|
+
model: "releases",
|
|
2250
|
+
onDelete: "none"
|
|
2251
|
+
}
|
|
2252
|
+
},
|
|
2253
|
+
created_at_ms: { type: "integer" },
|
|
2254
|
+
updated_at_ms: { type: "integer" }
|
|
2255
|
+
}, {
|
|
2256
|
+
key: ["id"],
|
|
2257
|
+
indexes: {
|
|
2258
|
+
byScope: {
|
|
2259
|
+
eq: ["scope_key"],
|
|
2260
|
+
sort: ["id"],
|
|
2261
|
+
root: { model: "release_catalogs" }
|
|
2262
|
+
},
|
|
2263
|
+
byScopeEnabled: {
|
|
2264
|
+
eq: ["scope_key", "enabled"],
|
|
2265
|
+
sort: ["id"],
|
|
2266
|
+
root: { model: "release_catalogs" }
|
|
2267
|
+
},
|
|
2268
|
+
byChannelPlatform: {
|
|
2269
|
+
eq: ["channel_id", "platform"],
|
|
2270
|
+
sort: ["id"]
|
|
2271
|
+
},
|
|
2272
|
+
byChannelPlatformEnabled: {
|
|
2273
|
+
eq: [
|
|
2274
|
+
"channel_id",
|
|
2275
|
+
"platform",
|
|
2276
|
+
"enabled"
|
|
2277
|
+
],
|
|
2278
|
+
sort: ["id"]
|
|
2279
|
+
},
|
|
2280
|
+
byBundle: {
|
|
2281
|
+
eq: ["bundle_id"],
|
|
2282
|
+
sort: ["id"]
|
|
2283
|
+
},
|
|
2284
|
+
all: {
|
|
2285
|
+
eq: [],
|
|
2286
|
+
sort: ["id"]
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
});
|
|
2290
|
+
/** One compiled catalog per scope; the update check reads exactly this row. */
|
|
2291
|
+
const releaseCatalogs = defineTable({
|
|
2292
|
+
scope_key: {
|
|
2293
|
+
type: "string",
|
|
2294
|
+
maxLength: 2048,
|
|
2295
|
+
ascii: true
|
|
2296
|
+
},
|
|
2297
|
+
catalog_id: {
|
|
2298
|
+
type: "string",
|
|
2299
|
+
maxLength: 255
|
|
2300
|
+
},
|
|
2301
|
+
strategy: {
|
|
2302
|
+
type: "string",
|
|
2303
|
+
maxLength: 16
|
|
2304
|
+
},
|
|
2305
|
+
channel_id: {
|
|
2306
|
+
...CHANNEL_ID,
|
|
2307
|
+
references: {
|
|
2308
|
+
model: "channels",
|
|
2309
|
+
onDelete: "none"
|
|
2310
|
+
}
|
|
2311
|
+
},
|
|
2312
|
+
channel_key: {
|
|
2313
|
+
type: "string",
|
|
2314
|
+
maxLength: 1400,
|
|
2315
|
+
ascii: true
|
|
2316
|
+
},
|
|
2317
|
+
platform: {
|
|
2318
|
+
type: "string",
|
|
2319
|
+
maxLength: 16
|
|
2320
|
+
},
|
|
2321
|
+
fingerprint_hash: {
|
|
2322
|
+
type: "string",
|
|
2323
|
+
maxLength: 255,
|
|
2324
|
+
required: false
|
|
2325
|
+
},
|
|
2326
|
+
generation: { type: "integer" },
|
|
2327
|
+
payload: { type: "string" },
|
|
2328
|
+
catalog_hash: {
|
|
2329
|
+
type: "string",
|
|
2330
|
+
maxLength: 71
|
|
2331
|
+
},
|
|
2332
|
+
byte_size: { type: "integer" },
|
|
2333
|
+
is_tombstone: { type: "boolean" },
|
|
2334
|
+
updated_at_ms: { type: "integer" }
|
|
2335
|
+
}, {
|
|
2336
|
+
key: ["scope_key"],
|
|
2337
|
+
indexes: { all: {
|
|
2338
|
+
eq: [],
|
|
2339
|
+
sort: ["scope_key"]
|
|
2340
|
+
} }
|
|
2341
|
+
});
|
|
2342
|
+
const channels = defineTable({
|
|
2343
|
+
id: CHANNEL_ID,
|
|
2344
|
+
name: {
|
|
2345
|
+
type: "string",
|
|
2346
|
+
maxLength: 255,
|
|
2347
|
+
unique: true
|
|
2348
|
+
}
|
|
2349
|
+
}, {
|
|
2350
|
+
key: ["id"],
|
|
2351
|
+
indexes: { all: {
|
|
2352
|
+
eq: [],
|
|
2353
|
+
sort: ["name"]
|
|
2354
|
+
} }
|
|
2355
|
+
});
|
|
2356
|
+
/**
|
|
2357
|
+
* The native builds `native-build android` and `native-build ios` track, newest first
|
|
2358
|
+
* in each scope: a platform, a channel, and the fingerprint or app version
|
|
2359
|
+
* the build compiled in. `min_bundle_id` names the build's built-in bundle,
|
|
2360
|
+
* a `bundles` row no release publishes. Its references keep no counters, so
|
|
2361
|
+
* adding this table changes no existing one; a channel with builds is
|
|
2362
|
+
* refused deletion through `byChannel`. `build_key` is the native build key
|
|
2363
|
+
* of every input the artifact was built from (`nbk1:` and a SHA-256), which
|
|
2364
|
+
* a build with the same inputs reuses it by; a build registered without
|
|
2365
|
+
* one is never reused. `build_number` is the artifact's `CFBundleVersion`
|
|
2366
|
+
* or `versionCode`. `submitted_to`, `submitted_at_ms` and
|
|
2367
|
+
* `submitted_version` record the store track a build went to, when, and
|
|
2368
|
+
* the store's label of the build; they stay null for a build no store took.
|
|
2369
|
+
*/
|
|
2370
|
+
const nativeBuilds = defineTable({
|
|
2371
|
+
id: {
|
|
2372
|
+
type: "string",
|
|
2373
|
+
maxLength: 36
|
|
2374
|
+
},
|
|
2375
|
+
scope_key: {
|
|
2376
|
+
type: "string",
|
|
2377
|
+
maxLength: 2048,
|
|
2378
|
+
ascii: true
|
|
2379
|
+
},
|
|
2380
|
+
platform: {
|
|
2381
|
+
type: "string",
|
|
2382
|
+
maxLength: 16
|
|
2383
|
+
},
|
|
2384
|
+
channel_id: {
|
|
2385
|
+
...CHANNEL_ID,
|
|
2386
|
+
references: {
|
|
2387
|
+
model: "channels",
|
|
2388
|
+
onDelete: "none"
|
|
2389
|
+
}
|
|
2390
|
+
},
|
|
2391
|
+
fingerprint_hash: {
|
|
2392
|
+
type: "string",
|
|
2393
|
+
maxLength: 255,
|
|
2394
|
+
required: false
|
|
2395
|
+
},
|
|
2396
|
+
app_version: {
|
|
2397
|
+
type: "string",
|
|
2398
|
+
required: false
|
|
2399
|
+
},
|
|
2400
|
+
min_bundle_id: {
|
|
2401
|
+
type: "string",
|
|
2402
|
+
maxLength: 36,
|
|
2403
|
+
unique: true,
|
|
2404
|
+
references: {
|
|
2405
|
+
model: "bundles",
|
|
2406
|
+
onDelete: "none"
|
|
2407
|
+
}
|
|
2408
|
+
},
|
|
2409
|
+
artifact_kind: {
|
|
2410
|
+
type: "string",
|
|
2411
|
+
maxLength: 16
|
|
2412
|
+
},
|
|
2413
|
+
artifact_storage_uri: { type: "string" },
|
|
2414
|
+
byte_size: { type: "integer" },
|
|
2415
|
+
git_commit_hash: {
|
|
2416
|
+
type: "string",
|
|
2417
|
+
maxLength: 255,
|
|
2418
|
+
required: false
|
|
2419
|
+
},
|
|
2420
|
+
created_at_ms: { type: "integer" },
|
|
2421
|
+
build_key: {
|
|
2422
|
+
type: "string",
|
|
2423
|
+
maxLength: 69,
|
|
2424
|
+
ascii: true,
|
|
2425
|
+
required: false
|
|
2426
|
+
},
|
|
2427
|
+
build_number: {
|
|
2428
|
+
type: "string",
|
|
2429
|
+
maxLength: 64,
|
|
2430
|
+
required: false
|
|
2431
|
+
},
|
|
2432
|
+
submitted_to: {
|
|
2433
|
+
type: "string",
|
|
2434
|
+
maxLength: 16,
|
|
2435
|
+
required: false
|
|
2436
|
+
},
|
|
2437
|
+
submitted_at_ms: {
|
|
2438
|
+
type: "integer",
|
|
2439
|
+
required: false
|
|
2440
|
+
},
|
|
2441
|
+
submitted_version: {
|
|
2442
|
+
type: "string",
|
|
2443
|
+
maxLength: 255,
|
|
2444
|
+
required: false
|
|
2445
|
+
}
|
|
2446
|
+
}, {
|
|
2447
|
+
key: ["id"],
|
|
2448
|
+
indexes: {
|
|
2449
|
+
byScope: {
|
|
2450
|
+
eq: ["scope_key"],
|
|
2451
|
+
sort: ["id"]
|
|
2452
|
+
},
|
|
2453
|
+
byBuildKey: {
|
|
2454
|
+
eq: ["scope_key", "build_key"],
|
|
2455
|
+
sort: ["id"]
|
|
2456
|
+
},
|
|
2457
|
+
byChannel: {
|
|
2458
|
+
eq: ["channel_id"],
|
|
2459
|
+
sort: ["id"],
|
|
2460
|
+
root: { model: "channels" }
|
|
2461
|
+
},
|
|
2462
|
+
all: {
|
|
2463
|
+
eq: [],
|
|
2464
|
+
sort: ["id"]
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
});
|
|
2069
2468
|
/** Core's module: always installed, never a plugin. */
|
|
2070
2469
|
const coreModule = {
|
|
2071
2470
|
id: "core",
|
|
2072
2471
|
schema: {
|
|
2073
|
-
bundles
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
platform: {
|
|
2079
|
-
type: "string",
|
|
2080
|
-
maxLength: 16
|
|
2081
|
-
},
|
|
2082
|
-
git_commit_hash: {
|
|
2083
|
-
type: "string",
|
|
2084
|
-
maxLength: 255,
|
|
2085
|
-
required: false
|
|
2086
|
-
},
|
|
2087
|
-
metadata: { type: "json" },
|
|
2088
|
-
manifest_storage_uri: { type: "string" },
|
|
2089
|
-
manifest_file_hash: { type: "string" },
|
|
2090
|
-
asset_base_storage_uri: { type: "string" }
|
|
2091
|
-
}, {
|
|
2092
|
-
key: ["id"],
|
|
2093
|
-
indexes: {
|
|
2094
|
-
byPlatform: {
|
|
2095
|
-
eq: ["platform"],
|
|
2096
|
-
sort: ["id"]
|
|
2097
|
-
},
|
|
2098
|
-
all: {
|
|
2099
|
-
eq: [],
|
|
2100
|
-
sort: ["id"]
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2103
|
-
}),
|
|
2104
|
-
bundle_patches: defineTable({
|
|
2105
|
-
id: {
|
|
2106
|
-
type: "string",
|
|
2107
|
-
maxLength: 255
|
|
2108
|
-
},
|
|
2109
|
-
bundle_id: {
|
|
2110
|
-
type: "string",
|
|
2111
|
-
maxLength: 36,
|
|
2112
|
-
references: {
|
|
2113
|
-
model: "bundles",
|
|
2114
|
-
onDelete: "cascade"
|
|
2115
|
-
}
|
|
2116
|
-
},
|
|
2117
|
-
base_bundle_id: {
|
|
2118
|
-
type: "string",
|
|
2119
|
-
maxLength: 36,
|
|
2120
|
-
references: {
|
|
2121
|
-
model: "bundles",
|
|
2122
|
-
onDelete: "cascade"
|
|
2123
|
-
}
|
|
2124
|
-
},
|
|
2125
|
-
base_file_hash: { type: "string" },
|
|
2126
|
-
patch_file_hash: { type: "string" },
|
|
2127
|
-
patch_storage_uri: { type: "string" },
|
|
2128
|
-
byte_size: { type: "integer" },
|
|
2129
|
-
order_index: { type: "integer" }
|
|
2130
|
-
}, {
|
|
2131
|
-
key: ["id"],
|
|
2132
|
-
indexes: {
|
|
2133
|
-
pair: {
|
|
2134
|
-
eq: ["bundle_id", "base_bundle_id"],
|
|
2135
|
-
sort: [],
|
|
2136
|
-
unique: true
|
|
2137
|
-
},
|
|
2138
|
-
byBundle: {
|
|
2139
|
-
eq: ["bundle_id"],
|
|
2140
|
-
sort: ["order_index"],
|
|
2141
|
-
root: { model: "bundles" }
|
|
2142
|
-
},
|
|
2143
|
-
byBase: {
|
|
2144
|
-
eq: ["base_bundle_id"],
|
|
2145
|
-
sort: ["bundle_id"]
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
}),
|
|
2149
|
-
releases: defineTable({
|
|
2150
|
-
id: {
|
|
2151
|
-
type: "string",
|
|
2152
|
-
maxLength: 36
|
|
2153
|
-
},
|
|
2154
|
-
revision: { type: "integer" },
|
|
2155
|
-
scope_key: {
|
|
2156
|
-
type: "string",
|
|
2157
|
-
maxLength: 2048,
|
|
2158
|
-
ascii: true
|
|
2159
|
-
},
|
|
2160
|
-
channel_id: {
|
|
2161
|
-
...CHANNEL_ID,
|
|
2162
|
-
references: {
|
|
2163
|
-
model: "channels",
|
|
2164
|
-
onDelete: "restrict"
|
|
2165
|
-
}
|
|
2166
|
-
},
|
|
2167
|
-
platform: {
|
|
2168
|
-
type: "string",
|
|
2169
|
-
maxLength: 16
|
|
2170
|
-
},
|
|
2171
|
-
kind: {
|
|
2172
|
-
type: "string",
|
|
2173
|
-
maxLength: 16
|
|
2174
|
-
},
|
|
2175
|
-
bundle_id: {
|
|
2176
|
-
type: "string",
|
|
2177
|
-
maxLength: 36,
|
|
2178
|
-
required: false,
|
|
2179
|
-
references: {
|
|
2180
|
-
model: "bundles",
|
|
2181
|
-
onDelete: "restrict"
|
|
2182
|
-
}
|
|
2183
|
-
},
|
|
2184
|
-
strategy: {
|
|
2185
|
-
type: "string",
|
|
2186
|
-
maxLength: 16
|
|
2187
|
-
},
|
|
2188
|
-
target_app_version: {
|
|
2189
|
-
type: "string",
|
|
2190
|
-
required: false
|
|
2191
|
-
},
|
|
2192
|
-
fingerprint_hash: {
|
|
2193
|
-
type: "string",
|
|
2194
|
-
maxLength: 255,
|
|
2195
|
-
required: false
|
|
2196
|
-
},
|
|
2197
|
-
enabled: { type: "boolean" },
|
|
2198
|
-
should_force_update: { type: "boolean" },
|
|
2199
|
-
message: {
|
|
2200
|
-
type: "string",
|
|
2201
|
-
required: false
|
|
2202
|
-
},
|
|
2203
|
-
rollout_cohort_count: { type: "integer" },
|
|
2204
|
-
target_cohorts: { type: "json" },
|
|
2205
|
-
operation: {
|
|
2206
|
-
type: "string",
|
|
2207
|
-
maxLength: 16
|
|
2208
|
-
},
|
|
2209
|
-
source_release_id: {
|
|
2210
|
-
type: "string",
|
|
2211
|
-
maxLength: 36,
|
|
2212
|
-
required: false,
|
|
2213
|
-
references: {
|
|
2214
|
-
model: "releases",
|
|
2215
|
-
onDelete: "none"
|
|
2216
|
-
}
|
|
2217
|
-
},
|
|
2218
|
-
created_at_ms: { type: "integer" },
|
|
2219
|
-
updated_at_ms: { type: "integer" }
|
|
2220
|
-
}, {
|
|
2221
|
-
key: ["id"],
|
|
2222
|
-
indexes: {
|
|
2223
|
-
byScope: {
|
|
2224
|
-
eq: ["scope_key"],
|
|
2225
|
-
sort: ["id"],
|
|
2226
|
-
root: { model: "release_catalogs" }
|
|
2227
|
-
},
|
|
2228
|
-
byScopeEnabled: {
|
|
2229
|
-
eq: ["scope_key", "enabled"],
|
|
2230
|
-
sort: ["id"],
|
|
2231
|
-
root: { model: "release_catalogs" }
|
|
2232
|
-
},
|
|
2233
|
-
byChannelPlatform: {
|
|
2234
|
-
eq: ["channel_id", "platform"],
|
|
2235
|
-
sort: ["id"]
|
|
2236
|
-
},
|
|
2237
|
-
byChannelPlatformEnabled: {
|
|
2238
|
-
eq: [
|
|
2239
|
-
"channel_id",
|
|
2240
|
-
"platform",
|
|
2241
|
-
"enabled"
|
|
2242
|
-
],
|
|
2243
|
-
sort: ["id"]
|
|
2244
|
-
},
|
|
2245
|
-
byBundle: {
|
|
2246
|
-
eq: ["bundle_id"],
|
|
2247
|
-
sort: ["id"]
|
|
2248
|
-
},
|
|
2249
|
-
all: {
|
|
2250
|
-
eq: [],
|
|
2251
|
-
sort: ["id"]
|
|
2252
|
-
}
|
|
2253
|
-
}
|
|
2254
|
-
}),
|
|
2255
|
-
release_catalogs: defineTable({
|
|
2256
|
-
scope_key: {
|
|
2257
|
-
type: "string",
|
|
2258
|
-
maxLength: 2048,
|
|
2259
|
-
ascii: true
|
|
2260
|
-
},
|
|
2261
|
-
catalog_id: {
|
|
2262
|
-
type: "string",
|
|
2263
|
-
maxLength: 255
|
|
2264
|
-
},
|
|
2265
|
-
strategy: {
|
|
2266
|
-
type: "string",
|
|
2267
|
-
maxLength: 16
|
|
2268
|
-
},
|
|
2269
|
-
channel_id: {
|
|
2270
|
-
...CHANNEL_ID,
|
|
2271
|
-
references: {
|
|
2272
|
-
model: "channels",
|
|
2273
|
-
onDelete: "none"
|
|
2274
|
-
}
|
|
2275
|
-
},
|
|
2276
|
-
channel_key: {
|
|
2277
|
-
type: "string",
|
|
2278
|
-
maxLength: 1400,
|
|
2279
|
-
ascii: true
|
|
2280
|
-
},
|
|
2281
|
-
platform: {
|
|
2282
|
-
type: "string",
|
|
2283
|
-
maxLength: 16
|
|
2284
|
-
},
|
|
2285
|
-
fingerprint_hash: {
|
|
2286
|
-
type: "string",
|
|
2287
|
-
maxLength: 255,
|
|
2288
|
-
required: false
|
|
2289
|
-
},
|
|
2290
|
-
generation: { type: "integer" },
|
|
2291
|
-
payload: { type: "string" },
|
|
2292
|
-
catalog_hash: {
|
|
2293
|
-
type: "string",
|
|
2294
|
-
maxLength: 71
|
|
2295
|
-
},
|
|
2296
|
-
byte_size: { type: "integer" },
|
|
2297
|
-
is_tombstone: { type: "boolean" },
|
|
2298
|
-
updated_at_ms: { type: "integer" }
|
|
2299
|
-
}, {
|
|
2300
|
-
key: ["scope_key"],
|
|
2301
|
-
indexes: { all: {
|
|
2302
|
-
eq: [],
|
|
2303
|
-
sort: ["scope_key"]
|
|
2304
|
-
} }
|
|
2305
|
-
}),
|
|
2306
|
-
channels: defineTable({
|
|
2307
|
-
id: CHANNEL_ID,
|
|
2308
|
-
name: {
|
|
2309
|
-
type: "string",
|
|
2310
|
-
maxLength: 255,
|
|
2311
|
-
unique: true
|
|
2312
|
-
}
|
|
2313
|
-
}, {
|
|
2314
|
-
key: ["id"],
|
|
2315
|
-
indexes: { all: {
|
|
2316
|
-
eq: [],
|
|
2317
|
-
sort: ["name"]
|
|
2318
|
-
} }
|
|
2319
|
-
}),
|
|
2472
|
+
bundles,
|
|
2473
|
+
bundle_patches: bundlePatches,
|
|
2474
|
+
releases,
|
|
2475
|
+
release_catalogs: releaseCatalogs,
|
|
2476
|
+
channels,
|
|
2320
2477
|
bundle_totals: defineAggregate({ platform_key: {
|
|
2321
2478
|
type: "string",
|
|
2322
2479
|
maxLength: 16
|
|
@@ -2327,7 +2484,8 @@ const coreModule = {
|
|
|
2327
2484
|
eq: ["platform_key"],
|
|
2328
2485
|
sort: []
|
|
2329
2486
|
} }
|
|
2330
|
-
})
|
|
2487
|
+
}),
|
|
2488
|
+
native_builds: nativeBuilds
|
|
2331
2489
|
}
|
|
2332
2490
|
};
|
|
2333
2491
|
//#endregion
|
|
@@ -3442,14 +3600,14 @@ const createEngineMigrator = (options) => {
|
|
|
3442
3600
|
return {
|
|
3443
3601
|
getVersion,
|
|
3444
3602
|
getNameVariants: async () => void 0,
|
|
3445
|
-
next: async () => await getVersion() === "1.
|
|
3603
|
+
next: async () => await getVersion() === "1.2.0" ? void 0 : { version: HOT_UPDATER_SCHEMA_VERSION },
|
|
3446
3604
|
previous: async () => void 0,
|
|
3447
3605
|
up: makeResult,
|
|
3448
3606
|
down: async () => {
|
|
3449
3607
|
throw new Error("No previous schema to migrate to.");
|
|
3450
3608
|
},
|
|
3451
3609
|
migrateTo: async (version, migrate) => {
|
|
3452
|
-
if (version !== "1.
|
|
3610
|
+
if (version !== "1.2.0") throw new Error(`Invalid version ${version}`);
|
|
3453
3611
|
return makeResult(migrate);
|
|
3454
3612
|
},
|
|
3455
3613
|
migrateToLatest: makeResult
|
|
@@ -4181,9 +4339,9 @@ const createDatabaseEngine = ({ batching, now = Date.now, meter, ...options }) =
|
|
|
4181
4339
|
*/
|
|
4182
4340
|
const pluginTablesFix = (database) => {
|
|
4183
4341
|
const { createMigrator, generateSchema } = database;
|
|
4184
|
-
const
|
|
4342
|
+
const doctor = "Run `npx hot-updater doctor` in the project whose hot-updater.config.ts names this database: it names the command that migrates it.";
|
|
4185
4343
|
const serverProject = "Run `hot-updater db` commands in the server's project: they load the server file named on the command line, or src/hotUpdater.* or src/db.*, and don't read hot-updater.config.ts.";
|
|
4186
|
-
return createMigrator !== void 0 ?
|
|
4344
|
+
return createMigrator !== void 0 ? `${doctor} In a server project, run \`hot-updater db migrate\`. ${serverProject}` : generateSchema !== void 0 ? `${doctor} On a database \`hot-updater init\` created, that is init again with its saved inputs; for your own server, run \`hot-updater db generate\` and apply the file it writes. ${serverProject}` : `${doctor} On a database \`hot-updater init\` created, that is init again with its saved inputs.`;
|
|
4187
4345
|
};
|
|
4188
4346
|
/**
|
|
4189
4347
|
* The storage engine over a database, as a server runs it: core's tables
|
|
@@ -4525,7 +4683,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
4525
4683
|
};
|
|
4526
4684
|
//#endregion
|
|
4527
4685
|
//#region ../../packages/server/dist/version.mjs
|
|
4528
|
-
const HOT_UPDATER_SERVER_VERSION = "1.
|
|
4686
|
+
const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.0";
|
|
4529
4687
|
//#endregion
|
|
4530
4688
|
//#region ../../packages/server/dist/handlerVersionRoutes.mjs
|
|
4531
4689
|
/**
|
|
@@ -5318,8 +5476,8 @@ const baseBundleIdsOf = (row, range, bundleId, limit) => {
|
|
|
5318
5476
|
//#endregion
|
|
5319
5477
|
//#region ../../packages/server/dist/core/reads.mjs
|
|
5320
5478
|
const PAGE$3 = 500;
|
|
5321
|
-
const pick = (fields) => (row) => Object.fromEntries(fields.map((field) => [field, row[field]]));
|
|
5322
|
-
const toBundleRow = pick([
|
|
5479
|
+
const pick$1 = (fields) => (row) => Object.fromEntries(fields.map((field) => [field, row[field]]));
|
|
5480
|
+
const toBundleRow = pick$1([
|
|
5323
5481
|
"id",
|
|
5324
5482
|
"platform",
|
|
5325
5483
|
"git_commit_hash",
|
|
@@ -5328,7 +5486,7 @@ const toBundleRow = pick([
|
|
|
5328
5486
|
"manifest_file_hash",
|
|
5329
5487
|
"asset_base_storage_uri"
|
|
5330
5488
|
]);
|
|
5331
|
-
const toPatchRow = pick([
|
|
5489
|
+
const toPatchRow = pick$1([
|
|
5332
5490
|
"id",
|
|
5333
5491
|
"bundle_id",
|
|
5334
5492
|
"base_bundle_id",
|
|
@@ -5338,7 +5496,7 @@ const toPatchRow = pick([
|
|
|
5338
5496
|
"byte_size",
|
|
5339
5497
|
"order_index"
|
|
5340
5498
|
]);
|
|
5341
|
-
const toReleaseRow = pick([
|
|
5499
|
+
const toReleaseRow = pick$1([
|
|
5342
5500
|
"id",
|
|
5343
5501
|
"revision",
|
|
5344
5502
|
"scope_key",
|
|
@@ -5359,7 +5517,7 @@ const toReleaseRow = pick([
|
|
|
5359
5517
|
"created_at_ms",
|
|
5360
5518
|
"updated_at_ms"
|
|
5361
5519
|
]);
|
|
5362
|
-
const toCatalogRow = pick([
|
|
5520
|
+
const toCatalogRow = pick$1([
|
|
5363
5521
|
"scope_key",
|
|
5364
5522
|
"catalog_id",
|
|
5365
5523
|
"strategy",
|
|
@@ -5374,7 +5532,7 @@ const toCatalogRow = pick([
|
|
|
5374
5532
|
"is_tombstone",
|
|
5375
5533
|
"updated_at_ms"
|
|
5376
5534
|
]);
|
|
5377
|
-
const toChannelRow = pick(["id", "name"]);
|
|
5535
|
+
const toChannelRow = pick$1(["id", "name"]);
|
|
5378
5536
|
/** Every page of one read, in order. */
|
|
5379
5537
|
const drain$1 = async (read) => {
|
|
5380
5538
|
const rows = [];
|
|
@@ -5386,6 +5544,8 @@ const drain$1 = async (read) => {
|
|
|
5386
5544
|
} while (cursor !== void 0);
|
|
5387
5545
|
return rows;
|
|
5388
5546
|
};
|
|
5547
|
+
/** A keyset page's range: the keys strictly after `after`, in `order`. */
|
|
5548
|
+
const after = (input) => input.after === void 0 ? {} : { range: (input.order ?? "asc") === "asc" ? { gt: input.after } : { lt: input.after } };
|
|
5389
5549
|
/** Core's reads, each through one declared index or key. */
|
|
5390
5550
|
const createCoreReads = (db, storage) => {
|
|
5391
5551
|
/** A bundle row's patches, exactly as many as its reference counter holds. */
|
|
@@ -5410,7 +5570,6 @@ const createCoreReads = (db, storage) => {
|
|
|
5410
5570
|
childCount: row._refs_bundle_patches_base_bundle_id ?? 0
|
|
5411
5571
|
};
|
|
5412
5572
|
};
|
|
5413
|
-
const after = (input) => input.after === void 0 ? {} : { range: (input.order ?? "asc") === "asc" ? { gt: input.after } : { lt: input.after } };
|
|
5414
5573
|
return {
|
|
5415
5574
|
/** The update check: one point read of its scope's catalog. */
|
|
5416
5575
|
async getReleaseCatalog(input) {
|
|
@@ -5601,9 +5760,13 @@ const insertBundle = (tx, row) => {
|
|
|
5601
5760
|
tx.create("bundles", row);
|
|
5602
5761
|
countBundle(tx, row.platform, 1);
|
|
5603
5762
|
};
|
|
5604
|
-
|
|
5763
|
+
/**
|
|
5764
|
+
* Updates a bundle; a platform change moves it between platform totals,
|
|
5765
|
+
* except for a native build's built-in bundle, which they never count.
|
|
5766
|
+
*/
|
|
5767
|
+
const updateBundle = (tx, current, set, { counted = true } = {}) => {
|
|
5605
5768
|
tx.update("bundles", current, set);
|
|
5606
|
-
if (set.platform !== void 0 && set.platform !== current.platform) {
|
|
5769
|
+
if (counted && set.platform !== void 0 && set.platform !== current.platform) {
|
|
5607
5770
|
countBundle(tx, current.platform, -1);
|
|
5608
5771
|
countBundle(tx, set.platform, 1);
|
|
5609
5772
|
}
|
|
@@ -5664,7 +5827,7 @@ const insertChannel = async (db, row) => {
|
|
|
5664
5827
|
throw error;
|
|
5665
5828
|
}
|
|
5666
5829
|
};
|
|
5667
|
-
/** Deletes a channel no release uses: 1 read
|
|
5830
|
+
/** Deletes a channel no release or native build uses: 1 read, 1 query, 1 write. */
|
|
5668
5831
|
const deleteChannel = (db, id) => db.transaction(async (tx) => {
|
|
5669
5832
|
const row = await tx.findOne("channels", { id });
|
|
5670
5833
|
if (row === null) return {
|
|
@@ -5675,10 +5838,243 @@ const deleteChannel = (db, id) => db.transaction(async (tx) => {
|
|
|
5675
5838
|
deleted: false,
|
|
5676
5839
|
reason: "not_empty"
|
|
5677
5840
|
};
|
|
5841
|
+
const { rows: builds } = await tx.findMany("native_builds", {
|
|
5842
|
+
index: "byChannel",
|
|
5843
|
+
where: { channel_id: id },
|
|
5844
|
+
limit: 1
|
|
5845
|
+
});
|
|
5846
|
+
if (builds.length > 0) return {
|
|
5847
|
+
deleted: false,
|
|
5848
|
+
reason: "not_empty"
|
|
5849
|
+
};
|
|
5678
5850
|
await tx.delete("channels", row);
|
|
5679
5851
|
return { deleted: true };
|
|
5680
5852
|
});
|
|
5681
5853
|
//#endregion
|
|
5854
|
+
//#region ../../packages/server/dist/core/nativeBuilds.mjs
|
|
5855
|
+
/** The artifacts each platform's build produces. */
|
|
5856
|
+
const ARTIFACT_KINDS = {
|
|
5857
|
+
android: ["apk", "aab"],
|
|
5858
|
+
ios: ["ipa", "app"]
|
|
5859
|
+
};
|
|
5860
|
+
/** `native_builds.scope_key`'s bound in core's schema. */
|
|
5861
|
+
const SCOPE_KEY_MAX_LENGTH = 2048;
|
|
5862
|
+
/** The `git_commit_hash` bound of `bundles` and `native_builds`. */
|
|
5863
|
+
const GIT_COMMIT_HASH_MAX_LENGTH = 255;
|
|
5864
|
+
/**
|
|
5865
|
+
* A native build key: `nbk1:` and a SHA-256, as the CLI computes it. The
|
|
5866
|
+
* format is the CLI's convention, implemented here so the registry stores
|
|
5867
|
+
* and looks up nothing else.
|
|
5868
|
+
*/
|
|
5869
|
+
const BUILD_KEY = /^nbk1:[0-9a-f]{64}$/;
|
|
5870
|
+
/** A native build or scope core cannot store or look up: the caller's input. */
|
|
5871
|
+
var NativeBuildInputError = class extends TypeError {
|
|
5872
|
+
name = "NativeBuildInputError";
|
|
5873
|
+
};
|
|
5874
|
+
const pick = (row) => ({
|
|
5875
|
+
id: row.id,
|
|
5876
|
+
scope_key: row.scope_key,
|
|
5877
|
+
platform: row.platform,
|
|
5878
|
+
channel_id: row.channel_id,
|
|
5879
|
+
fingerprint_hash: row.fingerprint_hash ?? null,
|
|
5880
|
+
app_version: row.app_version ?? null,
|
|
5881
|
+
min_bundle_id: row.min_bundle_id,
|
|
5882
|
+
artifact_kind: row.artifact_kind,
|
|
5883
|
+
artifact_storage_uri: row.artifact_storage_uri,
|
|
5884
|
+
byte_size: row.byte_size,
|
|
5885
|
+
git_commit_hash: row.git_commit_hash ?? null,
|
|
5886
|
+
created_at_ms: row.created_at_ms,
|
|
5887
|
+
build_key: row.build_key ?? null,
|
|
5888
|
+
build_number: row.build_number ?? null,
|
|
5889
|
+
submitted_to: row.submitted_to ?? null,
|
|
5890
|
+
submitted_at_ms: row.submitted_at_ms ?? null,
|
|
5891
|
+
submitted_version: row.submitted_version ?? null
|
|
5892
|
+
});
|
|
5893
|
+
/** The store track each platform's builds are submitted to. */
|
|
5894
|
+
const SUBMISSION_DESTINATIONS = {
|
|
5895
|
+
android: "play-internal",
|
|
5896
|
+
ios: "testflight"
|
|
5897
|
+
};
|
|
5898
|
+
/** `native_builds.build_number`'s bound in core's schema. */
|
|
5899
|
+
const BUILD_NUMBER_MAX_LENGTH = 64;
|
|
5900
|
+
/** `native_builds.submitted_version`'s bound in core's schema. */
|
|
5901
|
+
const SUBMITTED_VERSION_MAX_LENGTH = 255;
|
|
5902
|
+
/**
|
|
5903
|
+
* A native build scope's key: the Release Catalog scope key of a
|
|
5904
|
+
* fingerprint, or of the app-version strategy followed by the exact app
|
|
5905
|
+
* version the build compiled in. One key serves both strategies, so one
|
|
5906
|
+
* index finds the newest build of either.
|
|
5907
|
+
*/
|
|
5908
|
+
const nativeBuildScopeKey = (scope) => {
|
|
5909
|
+
let scopeKey;
|
|
5910
|
+
try {
|
|
5911
|
+
const channelKey = encodeChannelKey(scope.channel);
|
|
5912
|
+
scopeKey = scope.fingerprintHash === null ? `${createReleaseCatalogScopeKey({
|
|
5913
|
+
channelKey,
|
|
5914
|
+
platform: scope.platform,
|
|
5915
|
+
strategy: "APP_VERSION"
|
|
5916
|
+
})}:${encodeURIComponent(scope.appVersion)}` : createReleaseCatalogScopeKey({
|
|
5917
|
+
channelKey,
|
|
5918
|
+
fingerprintHash: scope.fingerprintHash,
|
|
5919
|
+
platform: scope.platform,
|
|
5920
|
+
strategy: "FINGERPRINT"
|
|
5921
|
+
});
|
|
5922
|
+
} catch (error) {
|
|
5923
|
+
throw new NativeBuildInputError(`Invalid native build scope: ${error instanceof Error ? error.message : String(error)}.`);
|
|
5924
|
+
}
|
|
5925
|
+
if (scopeKey.length > SCOPE_KEY_MAX_LENGTH) throw new NativeBuildInputError(`Invalid native build scope: its key is longer than ${SCOPE_KEY_MAX_LENGTH} characters; shorten the channel or app version.`);
|
|
5926
|
+
return scopeKey;
|
|
5927
|
+
};
|
|
5928
|
+
/** Whether `bundleId` is a native build's built-in bundle: one unique read. */
|
|
5929
|
+
const isBuiltInBundle = async (db, bundleId) => await db.findOne("native_builds", { min_bundle_id: bundleId }) !== null;
|
|
5930
|
+
const problemOf = ({ id, minBundleId, platform, channel, fingerprintHash, appVersion, gitCommitHash, buildKey, buildNumber, artifact }) => {
|
|
5931
|
+
if (!isUUIDv7(id)) return "id must be a UUIDv7";
|
|
5932
|
+
if (!isUUIDv7(minBundleId)) return "minBundleId must be the UUIDv7 a release build compiles in";
|
|
5933
|
+
if (platform !== "ios" && platform !== "android") return "platform must be ios or android";
|
|
5934
|
+
if (channel.trim().length === 0) return "channel is required";
|
|
5935
|
+
if (gitCommitHash !== null && gitCommitHash.length > GIT_COMMIT_HASH_MAX_LENGTH) return `gitCommitHash must be at most ${GIT_COMMIT_HASH_MAX_LENGTH} characters`;
|
|
5936
|
+
if (fingerprintHash === null === (appVersion === null)) return "exactly one of fingerprintHash and appVersion is required";
|
|
5937
|
+
if (buildKey !== null && !BUILD_KEY.test(buildKey)) return "buildKey must be nbk1: and a SHA-256, or null";
|
|
5938
|
+
if (buildNumber !== void 0 && buildNumber !== null && (buildNumber.trim().length === 0 || buildNumber.length > BUILD_NUMBER_MAX_LENGTH)) return `buildNumber must be the artifact's build number, up to ${BUILD_NUMBER_MAX_LENGTH} characters, or null`;
|
|
5939
|
+
const kinds = ARTIFACT_KINDS[platform];
|
|
5940
|
+
if (!kinds.includes(artifact.kind)) return `artifact.kind of an ${platform} build must be ${kinds.join(" or ")}`;
|
|
5941
|
+
if (!Number.isSafeInteger(artifact.byteSize) || artifact.byteSize < 0) return "artifact.byteSize must be a byte count";
|
|
5942
|
+
return null;
|
|
5943
|
+
};
|
|
5944
|
+
/** The native build registry: core's `native_builds` and the built-in bundle rows. */
|
|
5945
|
+
const createNativeBuildApi = (db, { now = Date.now } = {}) => ({
|
|
5946
|
+
async registerNativeBuild(registration) {
|
|
5947
|
+
const problem = problemOf(registration);
|
|
5948
|
+
if (problem !== null) throw new NativeBuildInputError(`Invalid native build: ${problem}.`);
|
|
5949
|
+
const scopeKey = nativeBuildScopeKey(registration);
|
|
5950
|
+
const channelId = `channel:${encodeChannelKey(registration.channel)}`;
|
|
5951
|
+
const { embeddedBundle } = registration;
|
|
5952
|
+
const row = {
|
|
5953
|
+
id: registration.id,
|
|
5954
|
+
scope_key: scopeKey,
|
|
5955
|
+
platform: registration.platform,
|
|
5956
|
+
channel_id: channelId,
|
|
5957
|
+
fingerprint_hash: registration.fingerprintHash,
|
|
5958
|
+
app_version: registration.appVersion,
|
|
5959
|
+
min_bundle_id: registration.minBundleId,
|
|
5960
|
+
artifact_kind: registration.artifact.kind,
|
|
5961
|
+
artifact_storage_uri: registration.artifact.storageUri,
|
|
5962
|
+
byte_size: registration.artifact.byteSize,
|
|
5963
|
+
git_commit_hash: registration.gitCommitHash,
|
|
5964
|
+
created_at_ms: now(),
|
|
5965
|
+
build_key: registration.buildKey,
|
|
5966
|
+
build_number: registration.buildNumber ?? null,
|
|
5967
|
+
submitted_to: null,
|
|
5968
|
+
submitted_at_ms: null,
|
|
5969
|
+
submitted_version: null
|
|
5970
|
+
};
|
|
5971
|
+
for (let attempt = 1;; attempt += 1) {
|
|
5972
|
+
await insertChannel(db, {
|
|
5973
|
+
id: channelId,
|
|
5974
|
+
name: registration.channel
|
|
5975
|
+
});
|
|
5976
|
+
try {
|
|
5977
|
+
await db.transaction(async (tx) => {
|
|
5978
|
+
tx.create("bundles", {
|
|
5979
|
+
id: registration.minBundleId,
|
|
5980
|
+
platform: registration.platform,
|
|
5981
|
+
git_commit_hash: registration.gitCommitHash,
|
|
5982
|
+
metadata: embeddedBundle.metadata ?? {},
|
|
5983
|
+
manifest_storage_uri: embeddedBundle.manifestStorageUri,
|
|
5984
|
+
manifest_file_hash: embeddedBundle.manifestFileHash,
|
|
5985
|
+
asset_base_storage_uri: embeddedBundle.assetBaseStorageUri
|
|
5986
|
+
});
|
|
5987
|
+
tx.create("native_builds", row);
|
|
5988
|
+
});
|
|
5989
|
+
break;
|
|
5990
|
+
} catch (error) {
|
|
5991
|
+
if (!(error instanceof DatabaseConstraintError)) throw error;
|
|
5992
|
+
if (error.reason === "exists" || error.reason === "unique") throw new DatabaseConstraintError(error.reason, "native_builds");
|
|
5993
|
+
if (attempt < 3 && error.reason === "not_found" && error.model === "channels") continue;
|
|
5994
|
+
throw error;
|
|
5995
|
+
}
|
|
5996
|
+
}
|
|
5997
|
+
return row;
|
|
5998
|
+
},
|
|
5999
|
+
async getNativeBuild(id) {
|
|
6000
|
+
const row = await db.findOne("native_builds", { id });
|
|
6001
|
+
return row === null ? null : pick(row);
|
|
6002
|
+
},
|
|
6003
|
+
async findNativeBuild(scope, options) {
|
|
6004
|
+
const scopeKey = nativeBuildScopeKey(scope);
|
|
6005
|
+
const buildKey = options?.buildKey;
|
|
6006
|
+
if (buildKey !== void 0 && !BUILD_KEY.test(buildKey)) throw new NativeBuildInputError("Invalid native build key: it must be nbk1: and a SHA-256.");
|
|
6007
|
+
const { rows } = buildKey === void 0 ? await db.findMany("native_builds", {
|
|
6008
|
+
index: "byScope",
|
|
6009
|
+
where: { scope_key: scopeKey },
|
|
6010
|
+
order: "desc",
|
|
6011
|
+
limit: 1
|
|
6012
|
+
}) : await db.findMany("native_builds", {
|
|
6013
|
+
index: "byBuildKey",
|
|
6014
|
+
where: {
|
|
6015
|
+
scope_key: scopeKey,
|
|
6016
|
+
build_key: buildKey
|
|
6017
|
+
},
|
|
6018
|
+
order: "desc",
|
|
6019
|
+
limit: 1
|
|
6020
|
+
});
|
|
6021
|
+
return rows[0] === void 0 ? null : pick(rows[0]);
|
|
6022
|
+
},
|
|
6023
|
+
async recordNativeBuildSubmission(id, { destination, version }) {
|
|
6024
|
+
if (version.trim().length === 0 || version.length > SUBMITTED_VERSION_MAX_LENGTH) throw new NativeBuildInputError(`Invalid native build submission: version must be the store's label of the build, up to ${SUBMITTED_VERSION_MAX_LENGTH} characters.`);
|
|
6025
|
+
return db.transaction(async (tx) => {
|
|
6026
|
+
const row = await tx.findOne("native_builds", { id });
|
|
6027
|
+
if (row === null) return null;
|
|
6028
|
+
const build = pick(row);
|
|
6029
|
+
if (SUBMISSION_DESTINATIONS[build.platform] !== destination) throw new NativeBuildInputError(`Invalid native build submission: an ${build.platform} build is submitted to ${SUBMISSION_DESTINATIONS[build.platform]}, not ${destination}.`);
|
|
6030
|
+
const set = {
|
|
6031
|
+
submitted_to: destination,
|
|
6032
|
+
submitted_at_ms: now(),
|
|
6033
|
+
submitted_version: version
|
|
6034
|
+
};
|
|
6035
|
+
tx.update("native_builds", row, set);
|
|
6036
|
+
return {
|
|
6037
|
+
...build,
|
|
6038
|
+
...set
|
|
6039
|
+
};
|
|
6040
|
+
});
|
|
6041
|
+
},
|
|
6042
|
+
async deleteNativeBuild(id) {
|
|
6043
|
+
return db.transaction(async (tx) => {
|
|
6044
|
+
const row = await tx.findOne("native_builds", { id });
|
|
6045
|
+
if (row === null) return {
|
|
6046
|
+
deleted: false,
|
|
6047
|
+
reason: "not_found"
|
|
6048
|
+
};
|
|
6049
|
+
await tx.delete("native_builds", row);
|
|
6050
|
+
const bundle = await tx.findOne("bundles", { id: row.min_bundle_id });
|
|
6051
|
+
if (bundle !== null) await tx.delete("bundles", bundle);
|
|
6052
|
+
return {
|
|
6053
|
+
deleted: true,
|
|
6054
|
+
nativeBuild: pick(row)
|
|
6055
|
+
};
|
|
6056
|
+
});
|
|
6057
|
+
},
|
|
6058
|
+
async listNativeBuilds(input) {
|
|
6059
|
+
const page = {
|
|
6060
|
+
order: input.order ?? "asc",
|
|
6061
|
+
limit: input.limit
|
|
6062
|
+
};
|
|
6063
|
+
const { rows } = input.channelId === void 0 ? await db.findMany("native_builds", {
|
|
6064
|
+
index: "all",
|
|
6065
|
+
where: {},
|
|
6066
|
+
...page,
|
|
6067
|
+
...after(input)
|
|
6068
|
+
}) : await db.findMany("native_builds", {
|
|
6069
|
+
index: "byChannel",
|
|
6070
|
+
where: { channel_id: input.channelId },
|
|
6071
|
+
...page,
|
|
6072
|
+
...after(input)
|
|
6073
|
+
});
|
|
6074
|
+
return rows.map(pick);
|
|
6075
|
+
}
|
|
6076
|
+
});
|
|
6077
|
+
//#endregion
|
|
5682
6078
|
//#region ../../packages/server/dist/core/releases.mjs
|
|
5683
6079
|
const PAGE$1 = 500;
|
|
5684
6080
|
const fail$1 = (code, message) => {
|
|
@@ -5992,6 +6388,7 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
|
|
|
5992
6388
|
if (!("bundleId" in deployment) || platforms.has(deployment.bundleId)) continue;
|
|
5993
6389
|
const row = await db.findOne("bundles", { id: deployment.bundleId });
|
|
5994
6390
|
if (row === null) throw new DatabaseBundleNotFoundError(deployment.bundleId);
|
|
6391
|
+
if (await isBuiltInBundle(db, deployment.bundleId)) throw new ReleaseManagementError("TARGET_RELEASE_INVALID", `Bundle "${deployment.bundleId}" is a native build's built-in bundle, which no release publishes.`);
|
|
5995
6392
|
platforms.set(deployment.bundleId, toBundleRow(row).platform);
|
|
5996
6393
|
}
|
|
5997
6394
|
const updatedAtMs = now();
|
|
@@ -6187,7 +6584,8 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
|
|
|
6187
6584
|
id
|
|
6188
6585
|
};
|
|
6189
6586
|
const { id: _id, ...set } = bundleToRow(next);
|
|
6190
|
-
|
|
6587
|
+
const counted = set.platform === current.platform || !await isBuiltInBundle(tx, id);
|
|
6588
|
+
updateBundle(tx, current, set, { counted });
|
|
6191
6589
|
if (patches !== void 0) await replaceBundlePatches(tx, current, bundleToPatchRows({
|
|
6192
6590
|
...next,
|
|
6193
6591
|
patches
|
|
@@ -6198,7 +6596,9 @@ const createCoreOperations = (db, { now = Date.now } = {}) => {
|
|
|
6198
6596
|
await db.transaction(async (tx) => {
|
|
6199
6597
|
for (const id of new Set(ids)) {
|
|
6200
6598
|
const current = await tx.findOne("bundles", { id });
|
|
6201
|
-
if (current
|
|
6599
|
+
if (current === null) continue;
|
|
6600
|
+
if (await isBuiltInBundle(tx, id)) throw new DatabaseRowReferencedError();
|
|
6601
|
+
await deleteBundle(tx, current);
|
|
6202
6602
|
}
|
|
6203
6603
|
});
|
|
6204
6604
|
} catch (error) {
|
|
@@ -6250,6 +6650,7 @@ const createCoreApi = (db, storage, options = {}) => {
|
|
|
6250
6650
|
return {
|
|
6251
6651
|
...reads,
|
|
6252
6652
|
...createCoreOperations(purgingCachedRoutes(db, options.onCachedRoutesChange), options),
|
|
6653
|
+
...createNativeBuildApi(db, options),
|
|
6253
6654
|
/** One read, which checks the schema settings first. */
|
|
6254
6655
|
ready: async () => {
|
|
6255
6656
|
await reads.listReleaseCatalogs({ limit: 1 });
|
|
@@ -6450,6 +6851,10 @@ const answer = async (operation) => {
|
|
|
6450
6851
|
code: "REFERENCED",
|
|
6451
6852
|
error: error.message
|
|
6452
6853
|
}, 409);
|
|
6854
|
+
if (error instanceof DatabaseConstraintError && error.model === "native_builds") return json$2({
|
|
6855
|
+
code: "NATIVE_BUILD_EXISTS",
|
|
6856
|
+
error: "A native build with this ID or built-in bundle ID is already registered."
|
|
6857
|
+
}, 409);
|
|
6453
6858
|
throw error;
|
|
6454
6859
|
}
|
|
6455
6860
|
};
|
|
@@ -6552,6 +6957,42 @@ const bundleUpdateOf = (value, bundleId) => {
|
|
|
6552
6957
|
if (id !== void 0 && id !== bundleId) throw new HandlerBadRequestError("Bundle id mismatch");
|
|
6553
6958
|
return update;
|
|
6554
6959
|
};
|
|
6960
|
+
const NATIVE_BUILD_ARTIFACT_KINDS = /* @__PURE__ */ new Set([
|
|
6961
|
+
"apk",
|
|
6962
|
+
"aab",
|
|
6963
|
+
"ipa",
|
|
6964
|
+
"app"
|
|
6965
|
+
]);
|
|
6966
|
+
/** A native build's scope: `fingerprintHash` or `appVersion`, never both. */
|
|
6967
|
+
const nativeBuildScopeOf = (url) => {
|
|
6968
|
+
const params = url.searchParams;
|
|
6969
|
+
const platform = platformOf(url);
|
|
6970
|
+
const channel = params.get("channel");
|
|
6971
|
+
const fingerprintHash = params.get("fingerprintHash");
|
|
6972
|
+
const appVersion = params.get("appVersion");
|
|
6973
|
+
if (platform === void 0 || !isText$2(channel) || fingerprintHash === null === (appVersion === null) || fingerprintHash === "" || appVersion === "") throw new HandlerBadRequestError("A native build lookup needs platform, channel, and fingerprintHash or appVersion.");
|
|
6974
|
+
return fingerprintHash !== null ? {
|
|
6975
|
+
platform,
|
|
6976
|
+
channel,
|
|
6977
|
+
fingerprintHash,
|
|
6978
|
+
appVersion: null
|
|
6979
|
+
} : {
|
|
6980
|
+
platform,
|
|
6981
|
+
channel,
|
|
6982
|
+
fingerprintHash: null,
|
|
6983
|
+
appVersion
|
|
6984
|
+
};
|
|
6985
|
+
};
|
|
6986
|
+
/** A native build or scope core refuses: a bad request. */
|
|
6987
|
+
const refusingInvalid = async (operation) => {
|
|
6988
|
+
try {
|
|
6989
|
+
return await operation();
|
|
6990
|
+
} catch (error) {
|
|
6991
|
+
if (error instanceof NativeBuildInputError) throw new HandlerBadRequestError(error.message);
|
|
6992
|
+
throw error;
|
|
6993
|
+
}
|
|
6994
|
+
};
|
|
6995
|
+
const isNativeBuildRegistration = (value) => isRecord$5(value) && isText$2(value.id) && isText$2(value.minBundleId) && typeof value.platform === "string" && isPlatform(value.platform) && isText$2(value.channel) && isTextOrNull$1(value.fingerprintHash) && isTextOrNull$1(value.appVersion) && value.fingerprintHash === null !== (value.appVersion === null) && isTextOrNull$1(value.gitCommitHash) && isTextOrNull$1(value.buildKey) && (value.buildNumber === void 0 || isTextOrNull$1(value.buildNumber)) && isRecord$5(value.artifact) && typeof value.artifact.kind === "string" && NATIVE_BUILD_ARTIFACT_KINDS.has(value.artifact.kind) && isText$2(value.artifact.storageUri) && Number.isSafeInteger(value.artifact.byteSize) && value.artifact.byteSize >= 0 && isRecord$5(value.embeddedBundle) && isText$2(value.embeddedBundle.manifestStorageUri) && isText$2(value.embeddedBundle.manifestFileHash) && isText$2(value.embeddedBundle.assetBaseStorageUri) && (value.embeddedBundle.metadata === void 0 || isRecord$5(value.embeddedBundle.metadata));
|
|
6555
6996
|
const scopeKeyOf = (params) => decodeMaybe(requireRouteParam(params, "scopeKey")) ?? "";
|
|
6556
6997
|
/** Every admin route's handler, by the name `ADMIN_ROUTES` mounts it under. */
|
|
6557
6998
|
const createAdminRouteHandlers = () => ({
|
|
@@ -6671,6 +7112,37 @@ const createAdminRouteHandlers = () => ({
|
|
|
6671
7112
|
}) });
|
|
6672
7113
|
}),
|
|
6673
7114
|
preflightReleaseCatalogRebuild: onCore(async (core, _request, params) => json$2({ data: await core.preflightReleaseCatalogRebuild(scopeKeyOf(params)) })),
|
|
7115
|
+
registerNativeBuild: onCore(async (core, request) => {
|
|
7116
|
+
const input = await body(request);
|
|
7117
|
+
if (!isNativeBuildRegistration(input)) throw new HandlerBadRequestError("Invalid native build");
|
|
7118
|
+
return json$2({ data: await refusingInvalid(() => core.registerNativeBuild(input)) }, 201);
|
|
7119
|
+
}),
|
|
7120
|
+
findNativeBuild: onCore(async (core, request) => {
|
|
7121
|
+
const url = new URL(request.url);
|
|
7122
|
+
const scope = nativeBuildScopeOf(url);
|
|
7123
|
+
const buildKey = url.searchParams.get("buildKey");
|
|
7124
|
+
return json$2({ data: await refusingInvalid(() => buildKey === null ? core.findNativeBuild(scope) : core.findNativeBuild(scope, { buildKey })) });
|
|
7125
|
+
}),
|
|
7126
|
+
getNativeBuild: onCore(async (core, _request, params) => json$2({ data: await core.getNativeBuild(requireRouteParam(params, "id")) })),
|
|
7127
|
+
listNativeBuilds: onCore(async (core, request) => {
|
|
7128
|
+
const url = new URL(request.url);
|
|
7129
|
+
const input = keyset(url);
|
|
7130
|
+
const channelId = url.searchParams.get("channelId");
|
|
7131
|
+
return page(await core.listNativeBuilds({
|
|
7132
|
+
...input,
|
|
7133
|
+
...channelId === null ? {} : { channelId }
|
|
7134
|
+
}), input, ({ id }) => id);
|
|
7135
|
+
}),
|
|
7136
|
+
recordNativeBuildSubmission: onCore(async (core, request, params) => {
|
|
7137
|
+
const input = await body(request);
|
|
7138
|
+
if (!isRecord$5(input) || input.destination !== "testflight" && input.destination !== "play-internal" || !isText$2(input.version)) throw new HandlerBadRequestError("A native build submission needs destination (testflight or play-internal) and version.");
|
|
7139
|
+
const submission = {
|
|
7140
|
+
destination: input.destination,
|
|
7141
|
+
version: input.version
|
|
7142
|
+
};
|
|
7143
|
+
return json$2({ data: await refusingInvalid(() => core.recordNativeBuildSubmission(requireRouteParam(params, "id"), submission)) });
|
|
7144
|
+
}),
|
|
7145
|
+
deleteNativeBuild: onCore(async (core, _request, params) => json$2({ data: await core.deleteNativeBuild(requireRouteParam(params, "id")) })),
|
|
6674
7146
|
deleteBundles: onCore(async (core, request) => {
|
|
6675
7147
|
const { ids } = await body(request);
|
|
6676
7148
|
if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && id.length > 0)) throw new HandlerBadRequestError("Bundle deletion needs ids");
|
|
@@ -6789,6 +7261,36 @@ const ADMIN_ROUTES = [
|
|
|
6789
7261
|
method: "GET",
|
|
6790
7262
|
path: "/base-candidates/:candidateKey",
|
|
6791
7263
|
handler: "findBaseCandidates"
|
|
7264
|
+
},
|
|
7265
|
+
{
|
|
7266
|
+
method: "POST",
|
|
7267
|
+
path: "/native-builds",
|
|
7268
|
+
handler: "registerNativeBuild"
|
|
7269
|
+
},
|
|
7270
|
+
{
|
|
7271
|
+
method: "GET",
|
|
7272
|
+
path: "/native-builds/latest",
|
|
7273
|
+
handler: "findNativeBuild"
|
|
7274
|
+
},
|
|
7275
|
+
{
|
|
7276
|
+
method: "GET",
|
|
7277
|
+
path: "/native-builds/:id",
|
|
7278
|
+
handler: "getNativeBuild"
|
|
7279
|
+
},
|
|
7280
|
+
{
|
|
7281
|
+
method: "GET",
|
|
7282
|
+
path: "/native-builds",
|
|
7283
|
+
handler: "listNativeBuilds"
|
|
7284
|
+
},
|
|
7285
|
+
{
|
|
7286
|
+
method: "POST",
|
|
7287
|
+
path: "/native-builds/:id/submission",
|
|
7288
|
+
handler: "recordNativeBuildSubmission"
|
|
7289
|
+
},
|
|
7290
|
+
{
|
|
7291
|
+
method: "DELETE",
|
|
7292
|
+
path: "/native-builds/:id",
|
|
7293
|
+
handler: "deleteNativeBuild"
|
|
6792
7294
|
}
|
|
6793
7295
|
];
|
|
6794
7296
|
//#endregion
|
|
@@ -11618,11 +12120,20 @@ const adapterOf = ({ databaseId, collection = FIREBASE_V1_COLLECTION, aggregateB
|
|
|
11618
12120
|
collection
|
|
11619
12121
|
}) });
|
|
11620
12122
|
};
|
|
12123
|
+
/**
|
|
12124
|
+
* The resource this database reaches, without credentials, under the key
|
|
12125
|
+
* `hot-updater doctor` reads to tell the database `hot-updater init` saved
|
|
12126
|
+
* from another (a convention CLAUDE.md documents).
|
|
12127
|
+
*/
|
|
12128
|
+
const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
|
|
11621
12129
|
/** Hot Updater's database in one Firestore collection, through the storage engine. */
|
|
11622
|
-
const firebaseDatabase = (config) =>
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
12130
|
+
const firebaseDatabase = (config) => ({
|
|
12131
|
+
...createEngineDatabase({
|
|
12132
|
+
name: "firebaseDatabase",
|
|
12133
|
+
adapter: adapterOf(config),
|
|
12134
|
+
aggregateBatching: config.aggregateBatching ?? {}
|
|
12135
|
+
}),
|
|
12136
|
+
...config.projectId === void 0 ? {} : { [DATABASE_TARGET]: `firestore/${config.projectId}/${config.databaseId ?? "(default)"}` }
|
|
11626
12137
|
});
|
|
11627
12138
|
//#endregion
|
|
11628
12139
|
//#region src/firebaseStorage.ts
|
|
@@ -11630,7 +12141,8 @@ const firebaseDatabase = (config) => createEngineDatabase({
|
|
|
11630
12141
|
const MAX_CACHED_SIGNED_URLS = 1e4;
|
|
11631
12142
|
const firebaseStorage = (config) => {
|
|
11632
12143
|
const app = (0, firebase_admin_app.getApps)().length ? (0, firebase_admin_app.getApp)() : (0, firebase_admin_app.initializeApp)(config);
|
|
11633
|
-
|
|
12144
|
+
let opened;
|
|
12145
|
+
const bucket = () => opened ??= (0, firebase_admin_storage.getStorage)(app).bucket(config.storageBucket);
|
|
11634
12146
|
const getStorageKey = createStorageKeyBuilder(config.basePath);
|
|
11635
12147
|
/**
|
|
11636
12148
|
* Signed URLs by object. Without a private key, as in Cloud Functions, each
|
|
@@ -11641,7 +12153,7 @@ const firebaseStorage = (config) => {
|
|
|
11641
12153
|
const signedUrls = /* @__PURE__ */ new Map();
|
|
11642
12154
|
const signUrl = async (key) => {
|
|
11643
12155
|
const expiresAt = Date.now() + (config.signedUrlExpiresIn ?? 3600) * 1e3;
|
|
11644
|
-
const [url] = await bucket.file(key).getSignedUrl({
|
|
12156
|
+
const [url] = await bucket().file(key).getSignedUrl({
|
|
11645
12157
|
action: "read",
|
|
11646
12158
|
expires: expiresAt
|
|
11647
12159
|
});
|
|
@@ -11674,11 +12186,11 @@ const firebaseStorage = (config) => {
|
|
|
11674
12186
|
protocol: "gs",
|
|
11675
12187
|
async put({ key, body, contentType }) {
|
|
11676
12188
|
const storageKey = getStorageKey(key);
|
|
11677
|
-
const
|
|
11678
|
-
await bucket.file(storageKey).
|
|
12189
|
+
const [{ Readable }, { pipeline }] = await Promise.all([import("node:stream"), import("node:stream/promises")]);
|
|
12190
|
+
await pipeline(Readable.fromWeb(body), bucket().file(storageKey).createWriteStream({ metadata: {
|
|
11679
12191
|
contentType,
|
|
11680
12192
|
cacheControl: "public, max-age=31536000, immutable"
|
|
11681
|
-
} });
|
|
12193
|
+
} }));
|
|
11682
12194
|
return { storageUri: createStorageUri({
|
|
11683
12195
|
bucket: config.storageBucket,
|
|
11684
12196
|
key: storageKey,
|
|
@@ -11688,7 +12200,7 @@ const firebaseStorage = (config) => {
|
|
|
11688
12200
|
async get({ storageUri }) {
|
|
11689
12201
|
const { key } = parseAndValidate(storageUri);
|
|
11690
12202
|
try {
|
|
11691
|
-
const file = bucket.file(key);
|
|
12203
|
+
const file = bucket().file(key);
|
|
11692
12204
|
const [[body], [metadata]] = await Promise.all([file.download(), file.getMetadata()]);
|
|
11693
12205
|
const headers = new Headers();
|
|
11694
12206
|
if (metadata.contentType) headers.set("content-type", metadata.contentType);
|
|
@@ -11713,12 +12225,12 @@ const firebaseStorage = (config) => {
|
|
|
11713
12225
|
},
|
|
11714
12226
|
async exists({ storageUri }) {
|
|
11715
12227
|
const { key } = parseAndValidate(storageUri);
|
|
11716
|
-
const [exists] = await bucket.file(key).exists();
|
|
12228
|
+
const [exists] = await bucket().file(key).exists();
|
|
11717
12229
|
return { exists };
|
|
11718
12230
|
},
|
|
11719
12231
|
async delete({ storageUri }) {
|
|
11720
12232
|
const { key } = parseAndValidate(storageUri);
|
|
11721
|
-
await bucket.file(key).delete({ ignoreNotFound: true });
|
|
12233
|
+
await bucket().file(key).delete({ ignoreNotFound: true });
|
|
11722
12234
|
return { deleted: true };
|
|
11723
12235
|
}
|
|
11724
12236
|
});
|