@hot-updater/plugin-remote-config 0.0.0-stage → 1.0.0-rc.42
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/LICENSE +33 -0
- package/README.md +23 -3
- package/client/package.json +7 -0
- package/dist/client/index.cjs +208 -0
- package/dist/client/index.d.cts +123 -0
- package/dist/client/index.d.mts +123 -0
- package/dist/client/index.mjs +207 -0
- package/dist/server/admin.cjs +96 -0
- package/dist/server/admin.d.cts +21 -0
- package/dist/server/admin.d.mts +21 -0
- package/dist/server/admin.mjs +96 -0
- package/dist/server/api.cjs +175 -0
- package/dist/server/api.d.cts +85 -0
- package/dist/server/api.d.mts +85 -0
- package/dist/server/api.mjs +174 -0
- package/dist/server/index.cjs +50 -0
- package/dist/server/index.d.cts +64 -0
- package/dist/server/index.d.mts +64 -0
- package/dist/server/index.mjs +36 -0
- package/dist/server/official.cjs +12 -0
- package/dist/server/official.mjs +12 -0
- package/dist/server/routes.cjs +157 -0
- package/dist/server/routes.mjs +157 -0
- package/dist/server/schema.cjs +46 -0
- package/dist/server/schema.d.cts +57 -0
- package/dist/server/schema.d.mts +57 -0
- package/dist/server/schema.mjs +46 -0
- package/dist/server/template.cjs +396 -0
- package/dist/server/template.d.cts +134 -0
- package/dist/server/template.d.mts +134 -0
- package/dist/server/template.mjs +387 -0
- package/dist/shared/ownEntry.cjs +8 -0
- package/dist/shared/ownEntry.mjs +8 -0
- package/dist/shared/wire.cjs +48 -0
- package/dist/shared/wire.d.cts +24 -0
- package/dist/shared/wire.d.mts +24 -0
- package/dist/shared/wire.mjs +45 -0
- package/package.json +59 -3
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const require_template = require("./template.cjs");
|
|
2
|
+
const require_api = require("./api.cjs");
|
|
3
|
+
const require_wire = require("../shared/wire.cjs");
|
|
4
|
+
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
5
|
+
//#region src/server/routes.ts
|
|
6
|
+
/** Seconds a client waits before it resends a request the busy database refused. */
|
|
7
|
+
const RETRY_AFTER_SECONDS = 5;
|
|
8
|
+
/** A shared cache keeps a device's values five seconds, as it keeps a Release Catalog. */
|
|
9
|
+
const FETCH_CACHE_CONTROL = "public, max-age=0, s-maxage=5";
|
|
10
|
+
const MAX_CONTEXT_LENGTH = 128;
|
|
11
|
+
var BadRequestError = class extends Error {};
|
|
12
|
+
const json = (body, status, headers = {}) => Response.json(body, {
|
|
13
|
+
headers: {
|
|
14
|
+
"cache-control": "private, no-store",
|
|
15
|
+
...headers
|
|
16
|
+
},
|
|
17
|
+
status
|
|
18
|
+
});
|
|
19
|
+
const run = async (operation) => {
|
|
20
|
+
try {
|
|
21
|
+
return await operation();
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error instanceof require_template.RemoteConfigValidationError) return json({
|
|
24
|
+
error: error.message,
|
|
25
|
+
issues: error.issues
|
|
26
|
+
}, 400);
|
|
27
|
+
if (error instanceof BadRequestError || error instanceof require_api.RemoteConfigInputError || error instanceof _hot_updater_plugin_core.DatabaseCursorError) return json({ error: error.message }, 400);
|
|
28
|
+
if ((0, _hot_updater_plugin_core.isDatabaseBusyError)(error)) {
|
|
29
|
+
console.warn("[hot-updater] Remote Config answered 503: the database is busy.", error);
|
|
30
|
+
return json({ error: "Service unavailable" }, 503, { "retry-after": String(RETRY_AFTER_SECONDS) });
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const readContext = (request) => {
|
|
36
|
+
const query = new URL(request.url).searchParams;
|
|
37
|
+
const read = (field) => {
|
|
38
|
+
const value = query.get(require_wire.REMOTE_CONFIG_QUERY[field]);
|
|
39
|
+
if (value === null || value.length === 0) return null;
|
|
40
|
+
if (value.length > MAX_CONTEXT_LENGTH) throw new BadRequestError(`${field} is too long.`);
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
const platform = read("platform");
|
|
44
|
+
if (platform !== null && platform !== "ios" && platform !== "android") throw new BadRequestError("platform must be \"ios\" or \"android\".");
|
|
45
|
+
return {
|
|
46
|
+
platform,
|
|
47
|
+
appVersion: read("appVersion"),
|
|
48
|
+
channel: read("channel"),
|
|
49
|
+
cohort: read("cohort"),
|
|
50
|
+
fingerprintHash: read("fingerprintHash")
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
const bodyHash = async (body) => {
|
|
54
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(body));
|
|
55
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
56
|
+
};
|
|
57
|
+
const readJsonBody = async (request) => {
|
|
58
|
+
const body = await request.json().catch(() => null);
|
|
59
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) throw new BadRequestError("The request body must be a JSON object.");
|
|
60
|
+
return body;
|
|
61
|
+
};
|
|
62
|
+
const versionParam = (params) => {
|
|
63
|
+
const text = params.version ?? "";
|
|
64
|
+
if (!/^\d{1,15}$/u.test(text)) throw new BadRequestError("The version must be a positive integer.");
|
|
65
|
+
return Number(text);
|
|
66
|
+
};
|
|
67
|
+
const publishResponse = (result) => result.status === "published" ? json(result.version, 200) : json({
|
|
68
|
+
error: `Version ${result.currentVersion} was published since; reload the template and publish again.`,
|
|
69
|
+
currentVersion: result.currentVersion
|
|
70
|
+
}, 409);
|
|
71
|
+
/**
|
|
72
|
+
* Remote Config's routes: the devices' `GET /remote-config` on
|
|
73
|
+
* `handlers.client`, and template management on `handlers.admin`.
|
|
74
|
+
*/
|
|
75
|
+
const createRemoteConfigEndpoints = (api) => [
|
|
76
|
+
{
|
|
77
|
+
method: "GET",
|
|
78
|
+
path: `/${require_wire.REMOTE_CONFIG_PATH}`,
|
|
79
|
+
access: "client",
|
|
80
|
+
handler: (request) => run(async () => {
|
|
81
|
+
const body = JSON.stringify(await api.resolve(readContext(request)));
|
|
82
|
+
const etag = `"sha256:${await bodyHash(body)}"`;
|
|
83
|
+
const headers = {
|
|
84
|
+
"cache-control": FETCH_CACHE_CONTROL,
|
|
85
|
+
"content-type": "application/json",
|
|
86
|
+
etag,
|
|
87
|
+
vary: "Accept-Encoding"
|
|
88
|
+
};
|
|
89
|
+
if (request.headers.get("if-none-match") === etag) return new Response(null, {
|
|
90
|
+
headers,
|
|
91
|
+
status: 304
|
|
92
|
+
});
|
|
93
|
+
return new Response(body, {
|
|
94
|
+
headers,
|
|
95
|
+
status: 200
|
|
96
|
+
});
|
|
97
|
+
})
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
method: "GET",
|
|
101
|
+
path: `/${require_wire.REMOTE_CONFIG_PATH}/template`,
|
|
102
|
+
access: "admin",
|
|
103
|
+
handler: () => run(async () => json(await api.getActive(), 200))
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
method: "PUT",
|
|
107
|
+
path: `/${require_wire.REMOTE_CONFIG_PATH}/template`,
|
|
108
|
+
access: "admin",
|
|
109
|
+
handler: (request) => run(async () => {
|
|
110
|
+
const body = await readJsonBody(request);
|
|
111
|
+
return publishResponse(await api.publish({
|
|
112
|
+
template: body.template,
|
|
113
|
+
baseVersion: body.baseVersion,
|
|
114
|
+
...body.description === void 0 ? {} : { description: body.description }
|
|
115
|
+
}));
|
|
116
|
+
})
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
method: "GET",
|
|
120
|
+
path: `/${require_wire.REMOTE_CONFIG_PATH}/versions`,
|
|
121
|
+
access: "admin",
|
|
122
|
+
handler: (request) => run(async () => {
|
|
123
|
+
const query = new URL(request.url).searchParams;
|
|
124
|
+
const limit = query.get("limit");
|
|
125
|
+
const cursor = query.get("cursor");
|
|
126
|
+
return json(await api.listVersions({
|
|
127
|
+
...limit === null ? {} : { limit: Number(limit) },
|
|
128
|
+
...cursor === null ? {} : { cursor }
|
|
129
|
+
}), 200);
|
|
130
|
+
})
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
method: "GET",
|
|
134
|
+
path: `/${require_wire.REMOTE_CONFIG_PATH}/versions/:version`,
|
|
135
|
+
access: "admin",
|
|
136
|
+
handler: (_request, params) => run(async () => {
|
|
137
|
+
const version = await api.getVersion(versionParam(params));
|
|
138
|
+
return version === null ? json({ error: "Version not found" }, 404) : json(version, 200);
|
|
139
|
+
})
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
method: "POST",
|
|
143
|
+
path: `/${require_wire.REMOTE_CONFIG_PATH}/versions/:version/rollback`,
|
|
144
|
+
access: "admin",
|
|
145
|
+
handler: (request, params) => run(async () => {
|
|
146
|
+
const body = await readJsonBody(request);
|
|
147
|
+
const result = await api.rollback({
|
|
148
|
+
version: versionParam(params),
|
|
149
|
+
baseVersion: body.baseVersion,
|
|
150
|
+
...body.description === void 0 ? {} : { description: body.description }
|
|
151
|
+
});
|
|
152
|
+
return result.status === "not_found" ? json({ error: "Version not found" }, 404) : publishResponse(result);
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
];
|
|
156
|
+
//#endregion
|
|
157
|
+
exports.createRemoteConfigEndpoints = createRemoteConfigEndpoints;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { RemoteConfigValidationError } from "./template.mjs";
|
|
2
|
+
import { RemoteConfigInputError } from "./api.mjs";
|
|
3
|
+
import { REMOTE_CONFIG_PATH, REMOTE_CONFIG_QUERY } from "../shared/wire.mjs";
|
|
4
|
+
import { DatabaseCursorError, isDatabaseBusyError } from "@hot-updater/plugin-core";
|
|
5
|
+
//#region src/server/routes.ts
|
|
6
|
+
/** Seconds a client waits before it resends a request the busy database refused. */
|
|
7
|
+
const RETRY_AFTER_SECONDS = 5;
|
|
8
|
+
/** A shared cache keeps a device's values five seconds, as it keeps a Release Catalog. */
|
|
9
|
+
const FETCH_CACHE_CONTROL = "public, max-age=0, s-maxage=5";
|
|
10
|
+
const MAX_CONTEXT_LENGTH = 128;
|
|
11
|
+
var BadRequestError = class extends Error {};
|
|
12
|
+
const json = (body, status, headers = {}) => Response.json(body, {
|
|
13
|
+
headers: {
|
|
14
|
+
"cache-control": "private, no-store",
|
|
15
|
+
...headers
|
|
16
|
+
},
|
|
17
|
+
status
|
|
18
|
+
});
|
|
19
|
+
const run = async (operation) => {
|
|
20
|
+
try {
|
|
21
|
+
return await operation();
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error instanceof RemoteConfigValidationError) return json({
|
|
24
|
+
error: error.message,
|
|
25
|
+
issues: error.issues
|
|
26
|
+
}, 400);
|
|
27
|
+
if (error instanceof BadRequestError || error instanceof RemoteConfigInputError || error instanceof DatabaseCursorError) return json({ error: error.message }, 400);
|
|
28
|
+
if (isDatabaseBusyError(error)) {
|
|
29
|
+
console.warn("[hot-updater] Remote Config answered 503: the database is busy.", error);
|
|
30
|
+
return json({ error: "Service unavailable" }, 503, { "retry-after": String(RETRY_AFTER_SECONDS) });
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const readContext = (request) => {
|
|
36
|
+
const query = new URL(request.url).searchParams;
|
|
37
|
+
const read = (field) => {
|
|
38
|
+
const value = query.get(REMOTE_CONFIG_QUERY[field]);
|
|
39
|
+
if (value === null || value.length === 0) return null;
|
|
40
|
+
if (value.length > MAX_CONTEXT_LENGTH) throw new BadRequestError(`${field} is too long.`);
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
const platform = read("platform");
|
|
44
|
+
if (platform !== null && platform !== "ios" && platform !== "android") throw new BadRequestError("platform must be \"ios\" or \"android\".");
|
|
45
|
+
return {
|
|
46
|
+
platform,
|
|
47
|
+
appVersion: read("appVersion"),
|
|
48
|
+
channel: read("channel"),
|
|
49
|
+
cohort: read("cohort"),
|
|
50
|
+
fingerprintHash: read("fingerprintHash")
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
const bodyHash = async (body) => {
|
|
54
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(body));
|
|
55
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
56
|
+
};
|
|
57
|
+
const readJsonBody = async (request) => {
|
|
58
|
+
const body = await request.json().catch(() => null);
|
|
59
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) throw new BadRequestError("The request body must be a JSON object.");
|
|
60
|
+
return body;
|
|
61
|
+
};
|
|
62
|
+
const versionParam = (params) => {
|
|
63
|
+
const text = params.version ?? "";
|
|
64
|
+
if (!/^\d{1,15}$/u.test(text)) throw new BadRequestError("The version must be a positive integer.");
|
|
65
|
+
return Number(text);
|
|
66
|
+
};
|
|
67
|
+
const publishResponse = (result) => result.status === "published" ? json(result.version, 200) : json({
|
|
68
|
+
error: `Version ${result.currentVersion} was published since; reload the template and publish again.`,
|
|
69
|
+
currentVersion: result.currentVersion
|
|
70
|
+
}, 409);
|
|
71
|
+
/**
|
|
72
|
+
* Remote Config's routes: the devices' `GET /remote-config` on
|
|
73
|
+
* `handlers.client`, and template management on `handlers.admin`.
|
|
74
|
+
*/
|
|
75
|
+
const createRemoteConfigEndpoints = (api) => [
|
|
76
|
+
{
|
|
77
|
+
method: "GET",
|
|
78
|
+
path: `/${REMOTE_CONFIG_PATH}`,
|
|
79
|
+
access: "client",
|
|
80
|
+
handler: (request) => run(async () => {
|
|
81
|
+
const body = JSON.stringify(await api.resolve(readContext(request)));
|
|
82
|
+
const etag = `"sha256:${await bodyHash(body)}"`;
|
|
83
|
+
const headers = {
|
|
84
|
+
"cache-control": FETCH_CACHE_CONTROL,
|
|
85
|
+
"content-type": "application/json",
|
|
86
|
+
etag,
|
|
87
|
+
vary: "Accept-Encoding"
|
|
88
|
+
};
|
|
89
|
+
if (request.headers.get("if-none-match") === etag) return new Response(null, {
|
|
90
|
+
headers,
|
|
91
|
+
status: 304
|
|
92
|
+
});
|
|
93
|
+
return new Response(body, {
|
|
94
|
+
headers,
|
|
95
|
+
status: 200
|
|
96
|
+
});
|
|
97
|
+
})
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
method: "GET",
|
|
101
|
+
path: `/${REMOTE_CONFIG_PATH}/template`,
|
|
102
|
+
access: "admin",
|
|
103
|
+
handler: () => run(async () => json(await api.getActive(), 200))
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
method: "PUT",
|
|
107
|
+
path: `/${REMOTE_CONFIG_PATH}/template`,
|
|
108
|
+
access: "admin",
|
|
109
|
+
handler: (request) => run(async () => {
|
|
110
|
+
const body = await readJsonBody(request);
|
|
111
|
+
return publishResponse(await api.publish({
|
|
112
|
+
template: body.template,
|
|
113
|
+
baseVersion: body.baseVersion,
|
|
114
|
+
...body.description === void 0 ? {} : { description: body.description }
|
|
115
|
+
}));
|
|
116
|
+
})
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
method: "GET",
|
|
120
|
+
path: `/${REMOTE_CONFIG_PATH}/versions`,
|
|
121
|
+
access: "admin",
|
|
122
|
+
handler: (request) => run(async () => {
|
|
123
|
+
const query = new URL(request.url).searchParams;
|
|
124
|
+
const limit = query.get("limit");
|
|
125
|
+
const cursor = query.get("cursor");
|
|
126
|
+
return json(await api.listVersions({
|
|
127
|
+
...limit === null ? {} : { limit: Number(limit) },
|
|
128
|
+
...cursor === null ? {} : { cursor }
|
|
129
|
+
}), 200);
|
|
130
|
+
})
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
method: "GET",
|
|
134
|
+
path: `/${REMOTE_CONFIG_PATH}/versions/:version`,
|
|
135
|
+
access: "admin",
|
|
136
|
+
handler: (_request, params) => run(async () => {
|
|
137
|
+
const version = await api.getVersion(versionParam(params));
|
|
138
|
+
return version === null ? json({ error: "Version not found" }, 404) : json(version, 200);
|
|
139
|
+
})
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
method: "POST",
|
|
143
|
+
path: `/${REMOTE_CONFIG_PATH}/versions/:version/rollback`,
|
|
144
|
+
access: "admin",
|
|
145
|
+
handler: (request, params) => run(async () => {
|
|
146
|
+
const body = await readJsonBody(request);
|
|
147
|
+
const result = await api.rollback({
|
|
148
|
+
version: versionParam(params),
|
|
149
|
+
baseVersion: body.baseVersion,
|
|
150
|
+
...body.description === void 0 ? {} : { description: body.description }
|
|
151
|
+
});
|
|
152
|
+
return result.status === "not_found" ? json({ error: "Version not found" }, 404) : publishResponse(result);
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
];
|
|
156
|
+
//#endregion
|
|
157
|
+
export { createRemoteConfigEndpoints };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
|
|
2
|
+
//#region src/server/schema.ts
|
|
3
|
+
/**
|
|
4
|
+
* Remote Config's tables: the active template in one row, which a device's
|
|
5
|
+
* fetch reads by key, and every published version beside it.
|
|
6
|
+
*/
|
|
7
|
+
const remoteConfigSchema = {
|
|
8
|
+
/** One row, `id` `"active"`: the template devices receive and its version. */
|
|
9
|
+
remote_config_active: (0, _hot_updater_plugin_core.defineTable)({
|
|
10
|
+
id: {
|
|
11
|
+
type: "string",
|
|
12
|
+
maxLength: 16
|
|
13
|
+
},
|
|
14
|
+
version: { type: "integer" },
|
|
15
|
+
template: { type: "json" },
|
|
16
|
+
updated_at_ms: { type: "integer" }
|
|
17
|
+
}, { key: ["id"] }),
|
|
18
|
+
/** Each publish and rollback, kept so the Console can show and restore it. */
|
|
19
|
+
remote_config_versions: (0, _hot_updater_plugin_core.defineTable)({
|
|
20
|
+
version: { type: "integer" },
|
|
21
|
+
template: { type: "json" },
|
|
22
|
+
description: {
|
|
23
|
+
type: "string",
|
|
24
|
+
maxLength: 256,
|
|
25
|
+
required: false
|
|
26
|
+
},
|
|
27
|
+
/** `PUBLISH`, or `ROLLBACK` for a copy of `rollback_source`. */
|
|
28
|
+
update_type: {
|
|
29
|
+
type: "string",
|
|
30
|
+
maxLength: 16
|
|
31
|
+
},
|
|
32
|
+
rollback_source: {
|
|
33
|
+
type: "integer",
|
|
34
|
+
required: false
|
|
35
|
+
},
|
|
36
|
+
created_at_ms: { type: "integer" }
|
|
37
|
+
}, {
|
|
38
|
+
key: ["version"],
|
|
39
|
+
indexes: { byVersion: {
|
|
40
|
+
eq: [],
|
|
41
|
+
sort: ["version"]
|
|
42
|
+
} }
|
|
43
|
+
})
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
exports.remoteConfigSchema = remoteConfigSchema;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//#region src/server/schema.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Remote Config's tables: the active template in one row, which a device's
|
|
4
|
+
* fetch reads by key, and every published version beside it.
|
|
5
|
+
*/
|
|
6
|
+
declare const remoteConfigSchema: {
|
|
7
|
+
/** One row, `id` `"active"`: the template devices receive and its version. */
|
|
8
|
+
readonly remote_config_active: import("@hot-updater/plugin-core").TableDefinition<{
|
|
9
|
+
readonly id: {
|
|
10
|
+
readonly type: "string";
|
|
11
|
+
readonly maxLength: 16;
|
|
12
|
+
};
|
|
13
|
+
readonly version: {
|
|
14
|
+
readonly type: "integer";
|
|
15
|
+
};
|
|
16
|
+
readonly template: {
|
|
17
|
+
readonly type: "json";
|
|
18
|
+
};
|
|
19
|
+
readonly updated_at_ms: {
|
|
20
|
+
readonly type: "integer";
|
|
21
|
+
};
|
|
22
|
+
}, {}, {}, readonly ["id"]>;
|
|
23
|
+
/** Each publish and rollback, kept so the Console can show and restore it. */
|
|
24
|
+
readonly remote_config_versions: import("@hot-updater/plugin-core").TableDefinition<{
|
|
25
|
+
readonly version: {
|
|
26
|
+
readonly type: "integer";
|
|
27
|
+
};
|
|
28
|
+
readonly template: {
|
|
29
|
+
readonly type: "json";
|
|
30
|
+
};
|
|
31
|
+
readonly description: {
|
|
32
|
+
readonly type: "string";
|
|
33
|
+
readonly maxLength: 256;
|
|
34
|
+
readonly required: false;
|
|
35
|
+
};
|
|
36
|
+
/** `PUBLISH`, or `ROLLBACK` for a copy of `rollback_source`. */
|
|
37
|
+
readonly update_type: {
|
|
38
|
+
readonly type: "string";
|
|
39
|
+
readonly maxLength: 16;
|
|
40
|
+
};
|
|
41
|
+
readonly rollback_source: {
|
|
42
|
+
readonly type: "integer";
|
|
43
|
+
readonly required: false;
|
|
44
|
+
};
|
|
45
|
+
readonly created_at_ms: {
|
|
46
|
+
readonly type: "integer";
|
|
47
|
+
};
|
|
48
|
+
}, {}, {
|
|
49
|
+
readonly byVersion: {
|
|
50
|
+
readonly eq: readonly [];
|
|
51
|
+
readonly sort: readonly ["version"];
|
|
52
|
+
};
|
|
53
|
+
}, readonly ["version"]>;
|
|
54
|
+
};
|
|
55
|
+
type RemoteConfigSchema = typeof remoteConfigSchema;
|
|
56
|
+
//#endregion
|
|
57
|
+
export { RemoteConfigSchema, remoteConfigSchema };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//#region src/server/schema.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Remote Config's tables: the active template in one row, which a device's
|
|
4
|
+
* fetch reads by key, and every published version beside it.
|
|
5
|
+
*/
|
|
6
|
+
declare const remoteConfigSchema: {
|
|
7
|
+
/** One row, `id` `"active"`: the template devices receive and its version. */
|
|
8
|
+
readonly remote_config_active: import("@hot-updater/plugin-core").TableDefinition<{
|
|
9
|
+
readonly id: {
|
|
10
|
+
readonly type: "string";
|
|
11
|
+
readonly maxLength: 16;
|
|
12
|
+
};
|
|
13
|
+
readonly version: {
|
|
14
|
+
readonly type: "integer";
|
|
15
|
+
};
|
|
16
|
+
readonly template: {
|
|
17
|
+
readonly type: "json";
|
|
18
|
+
};
|
|
19
|
+
readonly updated_at_ms: {
|
|
20
|
+
readonly type: "integer";
|
|
21
|
+
};
|
|
22
|
+
}, {}, {}, readonly ["id"]>;
|
|
23
|
+
/** Each publish and rollback, kept so the Console can show and restore it. */
|
|
24
|
+
readonly remote_config_versions: import("@hot-updater/plugin-core").TableDefinition<{
|
|
25
|
+
readonly version: {
|
|
26
|
+
readonly type: "integer";
|
|
27
|
+
};
|
|
28
|
+
readonly template: {
|
|
29
|
+
readonly type: "json";
|
|
30
|
+
};
|
|
31
|
+
readonly description: {
|
|
32
|
+
readonly type: "string";
|
|
33
|
+
readonly maxLength: 256;
|
|
34
|
+
readonly required: false;
|
|
35
|
+
};
|
|
36
|
+
/** `PUBLISH`, or `ROLLBACK` for a copy of `rollback_source`. */
|
|
37
|
+
readonly update_type: {
|
|
38
|
+
readonly type: "string";
|
|
39
|
+
readonly maxLength: 16;
|
|
40
|
+
};
|
|
41
|
+
readonly rollback_source: {
|
|
42
|
+
readonly type: "integer";
|
|
43
|
+
readonly required: false;
|
|
44
|
+
};
|
|
45
|
+
readonly created_at_ms: {
|
|
46
|
+
readonly type: "integer";
|
|
47
|
+
};
|
|
48
|
+
}, {}, {
|
|
49
|
+
readonly byVersion: {
|
|
50
|
+
readonly eq: readonly [];
|
|
51
|
+
readonly sort: readonly ["version"];
|
|
52
|
+
};
|
|
53
|
+
}, readonly ["version"]>;
|
|
54
|
+
};
|
|
55
|
+
type RemoteConfigSchema = typeof remoteConfigSchema;
|
|
56
|
+
//#endregion
|
|
57
|
+
export { RemoteConfigSchema, remoteConfigSchema };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineTable } from "@hot-updater/plugin-core";
|
|
2
|
+
//#region src/server/schema.ts
|
|
3
|
+
/**
|
|
4
|
+
* Remote Config's tables: the active template in one row, which a device's
|
|
5
|
+
* fetch reads by key, and every published version beside it.
|
|
6
|
+
*/
|
|
7
|
+
const remoteConfigSchema = {
|
|
8
|
+
/** One row, `id` `"active"`: the template devices receive and its version. */
|
|
9
|
+
remote_config_active: defineTable({
|
|
10
|
+
id: {
|
|
11
|
+
type: "string",
|
|
12
|
+
maxLength: 16
|
|
13
|
+
},
|
|
14
|
+
version: { type: "integer" },
|
|
15
|
+
template: { type: "json" },
|
|
16
|
+
updated_at_ms: { type: "integer" }
|
|
17
|
+
}, { key: ["id"] }),
|
|
18
|
+
/** Each publish and rollback, kept so the Console can show and restore it. */
|
|
19
|
+
remote_config_versions: defineTable({
|
|
20
|
+
version: { type: "integer" },
|
|
21
|
+
template: { type: "json" },
|
|
22
|
+
description: {
|
|
23
|
+
type: "string",
|
|
24
|
+
maxLength: 256,
|
|
25
|
+
required: false
|
|
26
|
+
},
|
|
27
|
+
/** `PUBLISH`, or `ROLLBACK` for a copy of `rollback_source`. */
|
|
28
|
+
update_type: {
|
|
29
|
+
type: "string",
|
|
30
|
+
maxLength: 16
|
|
31
|
+
},
|
|
32
|
+
rollback_source: {
|
|
33
|
+
type: "integer",
|
|
34
|
+
required: false
|
|
35
|
+
},
|
|
36
|
+
created_at_ms: { type: "integer" }
|
|
37
|
+
}, {
|
|
38
|
+
key: ["version"],
|
|
39
|
+
indexes: { byVersion: {
|
|
40
|
+
eq: [],
|
|
41
|
+
sort: ["version"]
|
|
42
|
+
} }
|
|
43
|
+
})
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
export { remoteConfigSchema };
|