@hot-updater/standalone 1.0.0-rc.3 → 1.0.0-rc.35

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 CHANGED
@@ -1,227 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
3
- //#region src/standaloneBundleRows.ts
4
- async function loadRows(remote, model) {
5
- return model === "bundles" ? remote.loadBundleRows() : (await remote.loadBundles()).flatMap(_hot_updater_plugin_core.bundleToPatchRows);
6
- }
7
- //#endregion
8
- //#region src/standaloneDatabaseQuery.ts
9
- const compare$1 = (left, right) => {
10
- if (typeof left === "number" && typeof right === "number") return left - right;
11
- return String(left).localeCompare(String(right));
12
- };
13
- const compareString = (actual, expected, mode, predicate) => {
14
- if (typeof actual !== "string") return false;
15
- return mode === "insensitive" ? predicate(actual.toLocaleLowerCase(), expected.toLocaleLowerCase()) : predicate(actual, expected);
16
- };
17
- const matchesCondition = (row, condition) => {
18
- const actual = Reflect.get(row, condition.field);
19
- const expected = Reflect.get(condition, "value");
20
- switch (Reflect.get(condition, "operator") ?? "eq") {
21
- case "eq":
22
- if (typeof expected === "string") return compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value === query);
23
- return actual === expected;
24
- case "ne":
25
- if (actual === null || actual === void 0) return false;
26
- if (typeof expected === "string") return !compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value === query);
27
- return actual !== expected;
28
- case "gt":
29
- if (actual === null || actual === void 0) return false;
30
- return compare$1(actual, expected) > 0;
31
- case "gte":
32
- if (actual === null || actual === void 0) return false;
33
- return compare$1(actual, expected) >= 0;
34
- case "lt":
35
- if (actual === null || actual === void 0) return false;
36
- return compare$1(actual, expected) < 0;
37
- case "lte":
38
- if (actual === null || actual === void 0) return false;
39
- return compare$1(actual, expected) <= 0;
40
- case "in": return Array.isArray(expected) && expected.some((candidate) => candidate === actual);
41
- case "not_in": return Array.isArray(expected) && (expected.length === 0 || actual !== null && actual !== void 0 && expected.every((candidate) => candidate !== actual));
42
- case "contains": return typeof expected === "string" ? compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value.includes(query)) : false;
43
- case "starts_with": return typeof expected === "string" ? compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value.startsWith(query)) : false;
44
- case "ends_with": return typeof expected === "string" ? compareString(actual, expected, Reflect.get(condition, "mode"), (value, query) => value.endsWith(query)) : false;
45
- default: return false;
46
- }
47
- };
48
- const matchesStandaloneWhere = (row, where) => {
49
- if (!where || where.length === 0) return true;
50
- let result = matchesCondition(row, where[0]);
51
- for (const condition of where.slice(1)) {
52
- const current = matchesCondition(row, condition);
53
- result = condition.connector === "OR" ? result || current : result && current;
54
- }
55
- return result;
56
- };
57
- const queryStandaloneRows = (rows, input) => {
58
- const filtered = rows.filter((row) => matchesStandaloneWhere(row, input.where));
59
- if (input.orderBy) {
60
- const direction = input.orderBy.direction === "asc" ? 1 : -1;
61
- filtered.sort((left, right) => compare$1(Reflect.get(left, input.orderBy?.field ?? "id"), Reflect.get(right, input.orderBy?.field ?? "id")) * direction);
62
- }
63
- const offset = input.offset ?? 0;
64
- return filtered.slice(offset, offset + (input.limit ?? filtered.length));
65
- };
66
- //#endregion
67
- //#region src/standaloneBundleReads.ts
68
- const compare = (left, right) => {
69
- if (typeof left === "number" && typeof right === "number") return left - right;
70
- return String(left).localeCompare(String(right));
71
- };
72
- const queryBundleRows = (rows, input) => {
73
- const filtered = queryStandaloneRows(rows, { where: input.where });
74
- const orderBy = input.orderBy;
75
- if (orderBy !== void 0) filtered.sort((left, right) => {
76
- for (const clause of orderBy) {
77
- const leftValue = Reflect.get(left, clause.field);
78
- const rightValue = Reflect.get(right, clause.field);
79
- if (leftValue === rightValue) continue;
80
- if (leftValue === null || rightValue === null) {
81
- const nulls = clause.nulls ?? (clause.direction === "asc" ? "last" : "first");
82
- return leftValue === null ? nulls === "first" ? -1 : 1 : nulls === "first" ? 1 : -1;
83
- }
84
- const result = compare(leftValue, rightValue);
85
- if (result !== 0) return clause.direction === "asc" ? result : -result;
86
- }
87
- return 0;
88
- });
89
- return (input.distinctOn === void 0 ? filtered : filtered.filter((row, index, ordered) => ordered.findIndex((candidate) => JSON.stringify(input.distinctOn?.fields.map((field) => Reflect.get(candidate, field))) === JSON.stringify(input.distinctOn?.fields.map((field) => Reflect.get(row, field)))) === index)).slice(input.offset, input.offset + input.limit);
90
- };
91
- const countDistinctRows = (rows, fields) => fields === void 0 ? rows.length : new Set(rows.map((row) => JSON.stringify(fields.map((field) => Reflect.get(row, field))))).size;
92
- const createBundleReads = (remote) => ({
93
- async count(input) {
94
- switch (input.model) {
95
- case "bundles": {
96
- const where = input.where;
97
- if (input.distinct === void 0) {
98
- const remoteWindow = await remote.loadBundleWindow({
99
- where,
100
- limit: 1,
101
- offset: 0
102
- });
103
- if (remoteWindow) return remoteWindow.total;
104
- }
105
- const rows = queryStandaloneRows(await loadRows(remote, "bundles"), { where });
106
- return countDistinctRows(rows, input.distinct);
107
- }
108
- case "bundle_patches": {
109
- const rows = queryStandaloneRows(await loadRows(remote, "bundle_patches"), { where: input.where });
110
- return countDistinctRows(rows, input.distinct);
111
- }
112
- }
113
- },
114
- async findOne(input) {
115
- if (input.model === "bundles") {
116
- const idSelector = input.where?.length === 1 ? input.where[0] : void 0;
117
- if (idSelector?.field === "id" && (idSelector.operator === void 0 || idSelector.operator === "eq") && typeof idSelector.value === "string") {
118
- const row = await remote.loadBundleRow(idSelector.value);
119
- return row && matchesStandaloneWhere(row, input.where) ? row : null;
120
- }
121
- return queryStandaloneRows(await loadRows(remote, "bundles"), {
122
- where: input.where,
123
- limit: 1
124
- })[0] ?? null;
125
- }
126
- return queryStandaloneRows(await loadRows(remote, "bundle_patches"), {
127
- where: input.where,
128
- limit: 1
129
- })[0] ?? null;
130
- },
131
- async findMany(input) {
132
- switch (input.model) {
133
- case "bundles": {
134
- const where = input.where;
135
- const orderBy = input.orderBy;
136
- const remoteSort = orderBy?.length === 1 && orderBy[0]?.field === "id" ? orderBy[0] : void 0;
137
- if (input.distinctOn === void 0 && (orderBy === void 0 || remoteSort)) {
138
- const remoteWindow = await remote.loadBundleWindow({
139
- where,
140
- limit: input.limit,
141
- offset: input.offset,
142
- orderBy: remoteSort
143
- });
144
- if (remoteWindow) return remoteWindow.rows;
145
- }
146
- return queryBundleRows(await loadRows(remote, "bundles"), {
147
- distinctOn: input.distinctOn,
148
- where,
149
- limit: input.limit,
150
- offset: input.offset,
151
- orderBy: input.orderBy
152
- });
153
- }
154
- case "bundle_patches": return queryBundleRows(await loadRows(remote, "bundle_patches"), {
155
- distinctOn: input.distinctOn,
156
- where: input.where,
157
- limit: input.limit,
158
- offset: input.offset,
159
- orderBy: input.orderBy
160
- });
161
- }
162
- }
163
- });
164
- //#endregion
165
- //#region src/standaloneBundleReader.ts
166
- const createStandaloneBundleReader = (remote) => createBundleReads(remote);
167
- //#endregion
168
- //#region src/standaloneBundleWhere.ts
169
- const appendBundleWhere = (url, where) => {
170
- const usedParameters = /* @__PURE__ */ new Set();
171
- const setParameter = (parameter, value) => {
172
- if (usedParameters.has(parameter)) return false;
173
- usedParameters.add(parameter);
174
- url.searchParams.set(parameter, value);
175
- return true;
176
- };
177
- const appendParameter = (parameter, values) => {
178
- if (values.length === 0 || usedParameters.has(parameter)) return false;
179
- usedParameters.add(parameter);
180
- for (const value of values) url.searchParams.append(parameter, value);
181
- return true;
182
- };
183
- for (const [index, condition] of (where ?? []).entries()) {
184
- if (index > 0 && condition.connector === "OR") return false;
185
- const operator = condition.operator ?? "eq";
186
- switch (condition.field) {
187
- case "platform":
188
- if (operator !== "eq" || typeof condition.value !== "string") return false;
189
- if (!setParameter("platform", condition.value)) return false;
190
- break;
191
- case "id": {
192
- let parameter;
193
- switch (operator) {
194
- case "eq":
195
- parameter = "idEq";
196
- break;
197
- case "gt":
198
- parameter = "idGt";
199
- break;
200
- case "gte":
201
- parameter = "idGte";
202
- break;
203
- case "lt":
204
- parameter = "idLt";
205
- break;
206
- case "lte": parameter = "idLte";
207
- }
208
- if (parameter && typeof condition.value === "string") {
209
- if (!setParameter(parameter, condition.value)) return false;
210
- break;
211
- }
212
- if (operator === "in" && Array.isArray(condition.value)) {
213
- if (!condition.value.every((value) => typeof value === "string")) return false;
214
- if (!appendParameter("idIn", condition.value)) return false;
215
- break;
216
- }
217
- return false;
218
- }
219
- default: return false;
220
- }
221
- }
222
- return true;
223
- };
224
- //#endregion
225
3
  //#region src/standaloneHttp.ts
226
4
  var StandaloneDatabaseError = class extends Error {
227
5
  code;
@@ -233,7 +11,7 @@ var StandaloneDatabaseError = class extends Error {
233
11
  this.status = status;
234
12
  }
235
13
  };
236
- const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
237
15
  const createStandaloneHttp = (config) => {
238
16
  const baseUrl = config.baseUrl.replace(/\/+$/, "");
239
17
  const buildUrl = (path) => `${baseUrl}${path}`;
@@ -246,8 +24,8 @@ const createStandaloneHttp = (config) => {
246
24
  let message = `Database request failed with status ${response.status}.`;
247
25
  try {
248
26
  const body = await response.json();
249
- if (isRecord$2(body) && typeof body.message === "string") message = body.message;
250
- else if (isRecord$2(body) && typeof body.error === "string") message = body.error;
27
+ if (isRecord$1(body) && typeof body.message === "string") message = body.message;
28
+ else if (isRecord$1(body) && typeof body.error === "string") message = body.error;
251
29
  } catch (error) {
252
30
  if (!(error instanceof SyntaxError)) throw error;
253
31
  }
@@ -261,409 +39,206 @@ const createStandaloneHttp = (config) => {
261
39
  throw new StandaloneDatabaseError("invalid-response", "Database response must contain JSON.", response.status);
262
40
  }
263
41
  };
264
- const load = async (route, searchParams, isResult, invalidMessage, signal) => {
265
- const url = new URL(buildUrl(route.path));
266
- for (const [key, value] of Object.entries(searchParams)) url.searchParams.set(key, value);
267
- const response = await fetch(url, {
268
- method: "GET",
269
- headers: headers(route.headers),
270
- signal
271
- });
272
- const value = await parseJson(response);
273
- if (!isResult(value)) throw new StandaloneDatabaseError("invalid-response", invalidMessage, response.status);
274
- return value;
275
- };
276
42
  return {
277
43
  buildUrl,
278
44
  headers,
279
- load,
280
- parseJson,
281
- requestFailed
45
+ parseJson
282
46
  };
283
47
  };
284
48
  //#endregion
285
- //#region src/standaloneResponseGuards.ts
286
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
287
- const isNonNegativeInteger = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
288
- const isByteSize = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
289
- const isBundlePatch = (value) => isRecord$1(value) && typeof value.baseBundleId === "string" && typeof value.baseFileHash === "string" && isByteSize(value.byteSize) && typeof value.patchFileHash === "string" && typeof value.patchStorageUri === "string";
290
- const isChannelText = (value) => {
291
- if (typeof value !== "string" || value.length === 0) return false;
292
- let codePoints = 0;
293
- for (const _codePoint of value) {
294
- codePoints += 1;
295
- if (codePoints > 255) return false;
49
+ //#region src/standaloneCore.ts
50
+ /** The admin API protocol this client speaks; the server's `/version` must report it or a later one. */
51
+ const STANDALONE_ADMIN_PROTOCOL = 2;
52
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
53
+ const MANAGEMENT_CODES = /* @__PURE__ */ new Set([
54
+ "ENABLED_RELEASE",
55
+ "RELEASE_NOT_FOUND",
56
+ "SCOPE_MOVE_UNSUPPORTED",
57
+ "TARGET_RELEASE_INVALID",
58
+ "VERSION_CONFLICT"
59
+ ]);
60
+ const CATALOG_CODES = /* @__PURE__ */ new Set([
61
+ "CATALOG_GENERATION_EXHAUSTED",
62
+ "CATALOG_IDENTITY_MISSING",
63
+ "INVALID_SCOPE",
64
+ "NON_MONOTONIC_RELEASE_ID"
65
+ ]);
66
+ /** A refusal the server named, as the error core throws in process; anything else as a request failure. */
67
+ const refusal = (status, value, fallback) => {
68
+ const code = isRecord(value) ? value.code : void 0;
69
+ const message = isRecord(value) && typeof value.error === "string" ? value.error : fallback;
70
+ if (typeof code === "string" && MANAGEMENT_CODES.has(code)) return new _hot_updater_plugin_core.ReleaseManagementError(code, message);
71
+ if (typeof code === "string" && CATALOG_CODES.has(code)) return new _hot_updater_plugin_core.ReleaseCatalogMutationError(code, message);
72
+ if (code === "BUNDLE_NOT_FOUND" && isRecord(value)) {
73
+ const id = /"([^"]+)"/u.exec(message)?.[1] ?? "";
74
+ return new _hot_updater_plugin_core.DatabaseBundleNotFoundError(id);
296
75
  }
297
- return true;
298
- };
299
- const isChannelRow = (value) => isRecord$1(value) && isChannelText(value.id) && isChannelText(value.name);
300
- const isBundle = (value) => isRecord$1(value) && typeof value.id === "string" && (value.platform === "ios" || value.platform === "android") && typeof value.fileHash === "string" && typeof value.storageUri === "string" && isByteSize(value.archiveByteSize) && (value.patches === void 0 || value.patches === null || Array.isArray(value.patches) && value.patches.every(isBundlePatch));
301
- const isPaginationInfo = (value) => isRecord$1(value) && isNonNegativeInteger(value.total) && typeof value.hasNextPage === "boolean" && typeof value.hasPreviousPage === "boolean" && isNonNegativeInteger(value.currentPage) && value.currentPage >= 1 && isNonNegativeInteger(value.totalPages);
302
- const isPaginatedResult = (value) => isRecord$1(value) && Array.isArray(value.data) && value.data.every(isBundle) && isPaginationInfo(value.pagination);
303
- const hasChannels = (value) => isRecord$1(value) && isRecord$1(value.data) && Array.isArray(value.data.channels) && value.data.channels.every(isChannelRow);
304
- const hasChannelInsertResult = (value) => isRecord$1(value) && isRecord$1(value.data) && isChannelRow(value.data.row) && typeof value.data.inserted === "boolean";
305
- const hasChannelDeleteResult = (value) => isRecord$1(value) && isRecord$1(value.data) && (value.data.deleted === true || value.data.deleted === false && (value.data.reason === "not_empty" || value.data.reason === "not_found"));
306
- //#endregion
307
- //#region src/standaloneRoutes.ts
308
- function bundlePath(bundleId) {
309
- return `/bundles/${encodeURIComponent(bundleId)}`;
310
- }
311
- function channelPath(channelId) {
312
- return `/channels/${encodeURIComponent(channelId)}`;
313
- }
314
- const defaultRoutes$1 = {
315
- create: () => ({ path: "/bundles" }),
316
- update: (bundleId) => ({ path: bundlePath(bundleId) }),
317
- list: () => ({
318
- path: "/bundles",
319
- headers: { "Cache-Control": "no-cache" }
320
- }),
321
- channels: () => ({
322
- path: "/channels",
323
- headers: { "Cache-Control": "no-cache" }
324
- }),
325
- deleteChannel: (channelId) => ({
326
- path: channelPath(channelId),
327
- headers: {}
328
- }),
329
- retrieve: (bundleId) => ({
330
- path: bundlePath(bundleId),
331
- headers: { Accept: "application/json" }
332
- }),
333
- delete: (bundleId) => ({ path: bundlePath(bundleId) })
76
+ return new StandaloneDatabaseError("request-failed", message, status);
334
77
  };
335
- const createRoute$1 = (defaultRoute, customRoute) => ({
336
- path: customRoute?.path ?? defaultRoute.path,
337
- headers: {
338
- ...defaultRoute.headers,
339
- ...customRoute?.headers
340
- }
78
+ const keysetParams = ({ order, after, limit }) => ({
79
+ v: String(2),
80
+ limit: String(limit),
81
+ ...order === void 0 ? {} : { order },
82
+ ...after === void 0 ? {} : { cursor: after }
341
83
  });
342
- //#endregion
343
- //#region src/standaloneBundleRemote.ts
344
- const PAGE_SIZE = 100;
345
- const createStandaloneBundleRemote = (config) => {
346
- const routes = {
347
- list: () => createRoute$1(defaultRoutes$1.list(), config.routes?.list?.()),
348
- channels: defaultRoutes$1.channels,
349
- deleteChannel: defaultRoutes$1.deleteChannel,
350
- create: () => createRoute$1(defaultRoutes$1.create(), config.routes?.create?.()),
351
- update: (bundleId) => createRoute$1(defaultRoutes$1.update(bundleId), config.routes?.update?.(bundleId)),
352
- retrieve: (bundleId) => createRoute$1(defaultRoutes$1.retrieve(bundleId), config.routes?.retrieve?.(bundleId)),
353
- delete: (bundleId) => createRoute$1(defaultRoutes$1.delete(bundleId), config.routes?.delete?.(bundleId))
354
- };
355
- const http = createStandaloneHttp(config);
356
- const loadBundles = async () => {
357
- const bundles = [];
358
- for (let page = 1;; page += 1) {
359
- const route = routes.list();
360
- const url = new URL(http.buildUrl(route.path));
361
- url.searchParams.set("limit", String(PAGE_SIZE));
362
- url.searchParams.set("page", String(page));
363
- const response = await fetch(url, {
364
- method: "GET",
365
- headers: http.headers(route.headers)
366
- });
367
- const value = await http.parseJson(response);
368
- if (!isPaginatedResult(value)) throw new StandaloneDatabaseError("invalid-response", "Invalid bundle list response.", response.status);
369
- bundles.push(...value.data);
370
- if (value.data.length < PAGE_SIZE || value.pagination.hasNextPage === false || bundles.length >= value.pagination.total) return bundles;
371
- }
372
- };
373
- const loadChannels = async () => {
374
- const route = routes.channels();
375
- const response = await fetch(http.buildUrl(route.path), {
376
- method: "GET",
377
- headers: http.headers(route.headers)
378
- });
379
- const value = await http.parseJson(response);
380
- if (!hasChannels(value)) throw new StandaloneDatabaseError("invalid-response", "Invalid channels response.", response.status);
381
- return value.data.channels;
382
- };
383
- const bundlesToRows = async (bundles) => bundles.map((bundle) => (0, _hot_updater_plugin_core.bundleToRow)(bundle));
384
- const loadBundleWindow = async (input) => {
385
- if (input.limit === 0) return {
386
- rows: [],
387
- total: 0
84
+ const filterParams = (filter) => {
85
+ switch (filter.kind) {
86
+ case "all": return {};
87
+ case "bundle": return { bundleId: filter.bundleId };
88
+ case "scope": return {
89
+ scopeKey: filter.scopeKey,
90
+ ...filter.enabled === void 0 ? {} : { enabled: String(filter.enabled) }
388
91
  };
389
- if (input.orderBy && input.orderBy.field !== "id") return null;
390
- const route = routes.list();
391
- const url = new URL(http.buildUrl(route.path));
392
- if (!appendBundleWhere(url, input.where)) return null;
393
- if (input.orderBy !== void 0) url.searchParams.set("orderDirection", input.orderBy.direction);
394
- const pageAligned = input.limit > 0 && input.offset % input.limit === 0;
395
- const remoteLimit = pageAligned ? input.limit : input.offset + input.limit;
396
- if (remoteLimit > PAGE_SIZE) return null;
397
- url.searchParams.set("limit", String(remoteLimit));
398
- url.searchParams.set("page", String(pageAligned ? input.offset / input.limit + 1 : 1));
399
- const response = await fetch(url, {
400
- method: "GET",
401
- headers: http.headers(route.headers)
402
- });
403
- const value = await http.parseJson(response);
404
- if (!isPaginatedResult(value)) throw new StandaloneDatabaseError("invalid-response", "Invalid bundle list response.", response.status);
405
- const bundles = pageAligned ? value.data : value.data.slice(input.offset, input.offset + input.limit);
406
- return {
407
- rows: await bundlesToRows(bundles),
408
- total: value.pagination.total
92
+ case "channelPlatform": return {
93
+ channelId: filter.channelId,
94
+ platform: filter.platform,
95
+ ...filter.enabled === void 0 ? {} : { enabled: String(filter.enabled) }
409
96
  };
97
+ }
98
+ };
99
+ /**
100
+ * Core's API over a self-hosted server's admin handler, protocol 2: the CLI's
101
+ * path to a database it cannot open itself. The first call checks that the
102
+ * server speaks protocol 2, so an older server fails fast.
103
+ */
104
+ const createStandaloneCoreApi = (config) => {
105
+ const http = createStandaloneHttp(config);
106
+ let checked;
107
+ const checkProtocol = async () => {
108
+ const response = await fetch(http.buildUrl("/version"), { headers: http.headers({ "Cache-Control": "no-cache" }) });
109
+ const body = response.status === 404 ? {} : await http.parseJson(response);
110
+ const protocol = isRecord(body) ? body.adminProtocol : void 0;
111
+ if (typeof protocol !== "number" || protocol < 2) throw new StandaloneDatabaseError("request-failed", `The server at ${config.baseUrl} speaks admin API protocol ${typeof protocol === "number" ? protocol : 1}, and this CLI needs 2. Upgrade @hot-updater/server on the server.`, response.status);
410
112
  };
411
- const insertChannel = async (input) => {
412
- const route = routes.channels();
413
- const response = await fetch(http.buildUrl(route.path), {
414
- method: "POST",
415
- headers: http.headers(route.headers),
416
- body: JSON.stringify(input)
113
+ /** One admin request's JSON body, or null for 204 and for a GET that finds nothing; `/version` is checked once before the first. */
114
+ const request = async (method, path, { query = {}, body } = {}) => {
115
+ checked ??= checkProtocol().catch((error) => {
116
+ checked = void 0;
117
+ throw error;
417
118
  });
418
- const value = await http.parseJson(response);
419
- if (!hasChannelInsertResult(value)) throw new StandaloneDatabaseError("invalid-response", "Invalid Channel insert response.", response.status);
420
- if (value.data.row.name !== input.row.name || value.data.inserted && value.data.row.id !== input.row.id) throw new StandaloneDatabaseError("invalid-response", "Invalid Channel insert response.", response.status);
421
- return value.data;
422
- };
423
- const deleteChannel = async (input) => {
424
- const route = routes.deleteChannel(input.id);
425
- const response = await fetch(http.buildUrl(route.path), {
426
- method: "DELETE",
427
- headers: http.headers(route.headers)
119
+ await checked;
120
+ const url = new URL(http.buildUrl(path));
121
+ for (const [key, value] of Object.entries(query)) url.searchParams.set(key, value);
122
+ const response = await fetch(url, {
123
+ method,
124
+ headers: http.headers(method === "GET" ? { "Cache-Control": "no-cache" } : void 0),
125
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
428
126
  });
429
- if (response.status === 204) return { deleted: true };
430
- let value;
431
- try {
432
- value = await response.json();
433
- } catch {
434
- throw new StandaloneDatabaseError("invalid-response", "Database response must contain JSON.", response.status);
127
+ if (response.status === 204) return null;
128
+ if (method === "GET" && response.status === 404) return null;
129
+ if (response.status === 503) throw new StandaloneDatabaseError("request-failed", `The server at ${config.baseUrl} answered ${method} ${path} with 503: its database is not ready. If it needs migrations, run \`hot-updater db migrate\` where the server's database is configured.`, 503);
130
+ if (!response.ok) {
131
+ const failure = await response.json().catch(() => null);
132
+ throw refusal(response.status, failure, `${method} ${path} failed with status ${response.status}.`);
435
133
  }
436
- if (!hasChannelDeleteResult(value)) throw new StandaloneDatabaseError("invalid-response", "Invalid Channel delete response.", response.status);
437
- return value.data;
438
- };
439
- const loadBundle = async (bundleId) => {
440
- const route = routes.retrieve(bundleId);
441
- const response = await fetch(http.buildUrl(route.path), {
442
- method: "GET",
443
- headers: http.headers(route.headers)
444
- });
445
- if (response.status === 404) return null;
446
134
  const value = await http.parseJson(response);
447
- if (!isBundle(value)) throw new StandaloneDatabaseError("invalid-response", "Invalid bundle response.", response.status);
135
+ if (!isRecord(value)) throw new StandaloneDatabaseError("invalid-response", `The server answered ${method} ${path} with no JSON object.`, response.status);
448
136
  return value;
449
137
  };
450
- const loadBundleRow = async (bundleId) => {
451
- const bundle = await loadBundle(bundleId);
452
- return bundle ? (await bundlesToRows([bundle]))[0] ?? null : null;
453
- };
454
- const loadBundleRows = async () => bundlesToRows(await loadBundles());
455
- const updateBundle = async (bundle) => {
456
- const route = routes.update(bundle.id);
457
- const response = await fetch(http.buildUrl(route.path), {
458
- method: "PATCH",
459
- headers: http.headers(route.headers),
460
- body: JSON.stringify(bundle)
461
- });
462
- await http.parseJson(response);
463
- };
464
- const createBundles = async (bundles) => {
465
- const route = routes.create();
466
- const response = await fetch(http.buildUrl(route.path), {
467
- method: "POST",
468
- headers: http.headers(route.headers),
469
- body: JSON.stringify(bundles)
470
- });
471
- await http.parseJson(response);
472
- };
473
- const deleteBundle = async (bundleId) => {
474
- const route = routes.delete(bundleId);
475
- const response = await fetch(http.buildUrl(route.path), {
476
- method: "DELETE",
477
- headers: http.headers(route.headers)
478
- });
479
- await http.parseJson(response);
480
- };
481
- return {
482
- createBundle: (bundle) => createBundles([bundle]),
483
- createBundles,
484
- deleteBundle,
485
- deleteChannel,
486
- insertChannel,
487
- loadBundle,
488
- loadBundleRow,
489
- loadBundleRows,
490
- loadBundles,
491
- loadBundleWindow,
492
- loadChannels,
493
- updateBundle
138
+ /** A request's `data`, or null when it found nothing. */
139
+ const call = async (...args) => {
140
+ const value = await request(...args);
141
+ if (value === null) return null;
142
+ if (!("data" in value)) throw new StandaloneDatabaseError("invalid-response", `The server answered ${args[0]} ${args[1]} without data.`);
143
+ return value.data;
494
144
  };
495
- };
496
- //#endregion
497
- //#region src/standaloneReleaseRemote.ts
498
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
499
- const data = (value, message) => {
500
- if (!isRecord(value) || !("data" in value)) throw new StandaloneDatabaseError("invalid-response", message);
501
- return value.data;
502
- };
503
- const createStandaloneReleaseRemote = (config) => {
504
- const http = createStandaloneHttp(config);
505
- const headers = () => http.headers({ "Cache-Control": "no-cache" });
145
+ const list = async (path, query) => await call("GET", path, { query }) ?? [];
146
+ const v2 = { v: String(2) };
147
+ const encode = encodeURIComponent;
506
148
  return {
507
- async findReleaseById(id) {
508
- const response = await fetch(http.buildUrl(`/releases/${encodeURIComponent(id)}`), { headers: headers() });
509
- if (response.status === 404) return null;
510
- return data(await http.parseJson(response), "Invalid Release response.");
149
+ ready: async () => {
150
+ await list("/release-catalogs", {
151
+ ...v2,
152
+ limit: "1"
153
+ });
511
154
  },
512
- async findReleasesByScope(input) {
513
- const url = new URL(http.buildUrl("/releases"));
514
- url.searchParams.set("scopeKey", input.scopeKey);
515
- url.searchParams.set("limit", String(input.limit));
516
- if (input.afterReleaseId !== void 0) url.searchParams.set("afterReleaseId", input.afterReleaseId);
517
- const response = await fetch(url, { headers: headers() });
518
- const rows = data(await http.parseJson(response), "Invalid Release list response.");
519
- if (!Array.isArray(rows)) throw new StandaloneDatabaseError("invalid-response", "Invalid Release list response.", response.status);
520
- return rows;
155
+ getBundle: (id) => call("GET", `/bundles/${encode(id)}`, { query: v2 }),
156
+ listBundles: ({ platform, ...input }) => list("/bundles", {
157
+ ...keysetParams(input),
158
+ ...platform === void 0 ? {} : { platform }
159
+ }),
160
+ countBundles: async (platform) => {
161
+ const total = await call("GET", "/bundles/count", { query: {
162
+ ...v2,
163
+ ...platform === void 0 ? {} : { platform }
164
+ } });
165
+ if (typeof total !== "number") throw new StandaloneDatabaseError("invalid-response", "The server answered a bundle count without a number.");
166
+ return total;
521
167
  },
522
- async findReleases(input) {
523
- const url = new URL(http.buildUrl("/releases"));
524
- url.searchParams.set("limit", String(input.limit));
525
- for (const [key, value] of Object.entries(input)) if (key !== "limit" && value !== void 0) url.searchParams.set(key, String(value));
526
- const response = await fetch(url, { headers: headers() });
527
- const rows = data(await http.parseJson(response), "Invalid Release list response.");
528
- if (!Array.isArray(rows)) throw new StandaloneDatabaseError("invalid-response", "Invalid Release list response.", response.status);
529
- return rows;
168
+ countBundleChildren: async (baseBundleIds) => {
169
+ const ids = [...new Set(baseBundleIds)];
170
+ if (ids.length === 0) return {};
171
+ return await call("GET", "/bundles/child-counts", { query: {
172
+ ...v2,
173
+ ids: ids.join(",")
174
+ } }) ?? {};
530
175
  },
531
- async findCatalogByScopeKey(scopeKey) {
532
- const response = await fetch(http.buildUrl(`/release-catalogs/${encodeURIComponent(scopeKey)}`), { headers: headers() });
533
- if (response.status === 404) return null;
534
- return data(await http.parseJson(response), "Invalid Release catalog response.");
176
+ listPatchesFromBase: (baseBundleId, input) => list(`/bundles/${encode(baseBundleId)}/children`, keysetParams(input)),
177
+ findBaseBundleIds: (candidateKey, bundleId, limit) => list(`/base-candidates/${encode(candidateKey)}`, {
178
+ before: bundleId,
179
+ limit: String(limit)
180
+ }),
181
+ getRelease: (id) => call("GET", `/releases/${encode(id)}`),
182
+ listReleases: ({ filter, ...input }) => list("/releases", {
183
+ ...keysetParams(input),
184
+ ...filterParams(filter)
185
+ }),
186
+ getReleaseCatalogRow: (scopeKey) => call("GET", `/release-catalogs/${encode(scopeKey)}`),
187
+ listReleaseCatalogs: (input) => list("/release-catalogs", keysetParams(input)),
188
+ listChannels: () => list("/channels", v2),
189
+ findChannelByName: (name) => call("GET", "/channels", { query: {
190
+ ...v2,
191
+ name
192
+ } }),
193
+ deploy: async (deployments) => await call("POST", "/releases", { body: { deployments } }),
194
+ updateReleasePolicy: async ({ releaseId, ...input }) => await call("PATCH", `/releases/${encode(releaseId)}`, { body: input }),
195
+ preflightReleasePolicy: async ({ releaseId, ...input }) => await call("POST", `/releases/${encode(releaseId)}/preflight`, { body: input }),
196
+ deleteRelease: async ({ releaseId, expectedRevision }) => await call("DELETE", `/releases/${encode(releaseId)}`, { query: {
197
+ confirm: releaseId,
198
+ ...expectedRevision === void 0 ? {} : { expectedRevision: String(expectedRevision) }
199
+ } }),
200
+ promoteRelease: async ({ releaseId, ...input }) => await call("POST", `/releases/${encode(releaseId)}/promote`, { body: input }),
201
+ rebuildReleaseCatalog: async (scopeKey) => await call("POST", `/release-catalogs/${encode(scopeKey)}/rebuild`),
202
+ preflightReleaseCatalogRebuild: async (scopeKey) => await call("POST", `/release-catalogs/${encode(scopeKey)}/preflight`),
203
+ ensureChannel: async (name) => await call("POST", "/channels", {
204
+ query: v2,
205
+ body: { name }
206
+ }),
207
+ deleteChannel: async (id) => {
208
+ return await call("DELETE", `/channels/${encode(id)}`).catch((error) => {
209
+ if (error instanceof StandaloneDatabaseError && error.status === 409) return {
210
+ deleted: false,
211
+ reason: "not_empty"
212
+ };
213
+ if (error instanceof StandaloneDatabaseError && error.status === 404) return {
214
+ deleted: false,
215
+ reason: "not_found"
216
+ };
217
+ throw error;
218
+ }) ?? { deleted: true };
535
219
  },
536
- async findCatalogs(input) {
537
- const url = new URL(http.buildUrl("/release-catalogs"));
538
- url.searchParams.set("limit", String(input.limit));
539
- if (input.afterScopeKey !== void 0) url.searchParams.set("afterScopeKey", input.afterScopeKey);
540
- const response = await fetch(url, { headers: headers() });
541
- const rows = data(await http.parseJson(response), "Invalid Release catalog list response.");
542
- if (!Array.isArray(rows)) throw new StandaloneDatabaseError("invalid-response", "Invalid Release catalog list response.", response.status);
543
- return rows;
220
+ updateBundle: async (id, update) => {
221
+ await request("PATCH", `/bundles/${encode(id)}`, { body: update });
544
222
  },
545
- async commit(input) {
546
- const response = await fetch(http.buildUrl("/database/commit"), {
547
- body: JSON.stringify(input),
548
- headers: http.headers(),
549
- method: "POST"
550
- });
551
- return data(await http.parseJson(response), "Invalid database commit response.");
223
+ deleteBundles: async (ids) => {
224
+ await request("POST", "/bundles/delete", { body: { ids } });
552
225
  }
553
226
  };
554
227
  };
555
228
  //#endregion
556
229
  //#region src/standaloneRepository.ts
557
- const toBundleWhere = (where) => {
558
- if (!where) return [];
559
- const filters = [];
560
- if (where.platform !== void 0) filters.push({
561
- field: "platform",
562
- value: where.platform
563
- });
564
- if (where.id?.eq !== void 0) filters.push({
565
- field: "id",
566
- value: where.id.eq
567
- });
568
- if (where.id?.gt !== void 0) filters.push({
569
- field: "id",
570
- operator: "gt",
571
- value: where.id.gt
572
- });
573
- if (where.id?.gte !== void 0) filters.push({
574
- field: "id",
575
- operator: "gte",
576
- value: where.id.gte
577
- });
578
- if (where.id?.lt !== void 0) filters.push({
579
- field: "id",
580
- operator: "lt",
581
- value: where.id.lt
582
- });
583
- if (where.id?.lte !== void 0) filters.push({
584
- field: "id",
585
- operator: "lte",
586
- value: where.id.lte
587
- });
588
- if (where.id?.in !== void 0) filters.push({
589
- field: "id",
590
- operator: "in",
591
- value: where.id.in
592
- });
593
- return filters;
594
- };
595
230
  /**
596
- * Bundle-only HTTP repository used by the CLI for a self-hosted server.
597
- *
598
- * This is intentionally not a database plugin: insights and access-key
599
- * persistence belong to the server's database provider.
231
+ * A self-hosted server's database for the CLI and console: core's API over
232
+ * the server's admin API protocol 2, and GETs on its admin handler for the
233
+ * routes core does not cover, which its plugins serve. Plugins' tables belong
234
+ * to the server's own database.
600
235
  */
601
236
  const standaloneRepository = (config) => {
602
- const remote = createStandaloneBundleRemote(config);
603
- const releaseRemote = createStandaloneReleaseRemote(config);
604
- const bundleReader = createStandaloneBundleReader(remote);
237
+ const http = createStandaloneHttp(config);
605
238
  return Object.freeze({
606
239
  name: "standalone-repository",
607
- models: {
608
- bundles: {
609
- async findById(id) {
610
- return await bundleReader.findOne({
611
- model: "bundles",
612
- where: [{
613
- field: "id",
614
- value: id
615
- }]
616
- });
617
- },
618
- async findMany(query) {
619
- return await bundleReader.findMany({
620
- model: "bundles",
621
- where: toBundleWhere(query.where),
622
- limit: query.limit,
623
- offset: query.offset,
624
- orderBy: [query.orderBy]
625
- });
626
- },
627
- count: (where) => bundleReader.count({
628
- model: "bundles",
629
- where: toBundleWhere(where)
630
- })
631
- },
632
- bundlePatches: { async findByBundleIds(bundleIds) {
633
- if (bundleIds.length === 0) return [];
634
- return await bundleReader.findMany({
635
- model: "bundle_patches",
636
- where: [{
637
- field: "bundle_id",
638
- operator: "in",
639
- value: bundleIds
640
- }],
641
- orderBy: [{
642
- field: "id",
643
- direction: "asc"
644
- }],
645
- limit: Number.MAX_SAFE_INTEGER,
646
- offset: 0
647
- });
648
- } },
649
- releases: {
650
- findById: (id) => releaseRemote.findReleaseById(id),
651
- findMany: (input) => releaseRemote.findReleases(input),
652
- findManyByScope: (input) => releaseRemote.findReleasesByScope(input)
653
- },
654
- releaseCatalogs: {
655
- findByScopeKey: (scopeKey) => releaseRemote.findCatalogByScopeKey(scopeKey),
656
- findMany: (input) => releaseRemote.findCatalogs(input)
657
- },
658
- channels: {
659
- insert: (input) => remote.insertChannel(input),
660
- delete: (input) => remote.deleteChannel(input),
661
- async list() {
662
- return { channels: await remote.loadChannels() };
663
- }
664
- }
665
- },
666
- commit: (input) => releaseRemote.commit(input)
240
+ core: createStandaloneCoreApi(config),
241
+ fetchAdmin: (path) => fetch(http.buildUrl(path), { headers: http.headers({ "Cache-Control": "no-cache" }) })
667
242
  });
668
243
  };
669
244
  //#endregion
@@ -705,7 +280,7 @@ const standaloneStorage = (config) => {
705
280
  if (!response.ok && response.status !== 404) throw new Error(`Failed to ${routeName} storage object: ${response.statusText}`);
706
281
  return response;
707
282
  };
708
- return (0, _hot_updater_plugin_core.createStoragePlugin)({
283
+ return (0, _hot_updater_plugin_core.createStorageAdapter)({
709
284
  name: "standaloneStorage",
710
285
  protocol: config.protocol,
711
286
  async put({ key, body, contentType, contentLength }) {
@@ -746,6 +321,8 @@ const standaloneStorage = (config) => {
746
321
  });
747
322
  };
748
323
  //#endregion
324
+ exports.STANDALONE_ADMIN_PROTOCOL = STANDALONE_ADMIN_PROTOCOL;
749
325
  exports.StandaloneDatabaseError = StandaloneDatabaseError;
326
+ exports.createStandaloneCoreApi = createStandaloneCoreApi;
750
327
  exports.standaloneRepository = standaloneRepository;
751
328
  exports.standaloneStorage = standaloneStorage;