@hot-updater/server 1.0.0-rc.16 → 1.0.0-rc.18
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/adapters/drizzle.cjs +5 -5
- package/dist/adapters/drizzle.mjs +4 -4
- package/dist/adapters/kysely.cjs +3 -3
- package/dist/adapters/kysely.mjs +2 -2
- package/dist/adapters/mongodb.cjs +3 -3
- package/dist/adapters/mongodb.mjs +2 -2
- package/dist/adapters/mongodbAdapter.cjs +19 -1
- package/dist/adapters/mongodbAdapter.mjs +19 -1
- package/dist/adapters/prisma.cjs +5 -5
- package/dist/adapters/prisma.mjs +4 -4
- package/dist/assembly/assemblePlugins.cjs +47 -28
- package/dist/assembly/assemblePlugins.d.cts +3 -7
- package/dist/assembly/assemblePlugins.d.mts +3 -7
- package/dist/assembly/assemblePlugins.mjs +46 -27
- package/dist/assembly/clientPlugins.cjs +86 -0
- package/dist/assembly/clientPlugins.d.cts +16 -0
- package/dist/assembly/clientPlugins.d.mts +16 -0
- package/dist/assembly/clientPlugins.mjs +86 -0
- package/dist/assembly/configError.cjs +7 -0
- package/dist/assembly/configError.d.cts +7 -0
- package/dist/assembly/configError.d.mts +7 -0
- package/dist/assembly/configError.mjs +7 -0
- package/dist/assembly/databasePlugins.cjs +5 -1
- package/dist/assembly/databasePlugins.d.cts +5 -5
- package/dist/assembly/databasePlugins.d.mts +5 -5
- package/dist/assembly/databasePlugins.mjs +5 -1
- package/dist/assembly/pluginCli.cjs +125 -0
- package/dist/assembly/pluginCli.d.cts +59 -0
- package/dist/assembly/pluginCli.d.mts +59 -0
- package/dist/assembly/pluginCli.mjs +121 -0
- package/dist/createHotUpdaterCore.cjs +14 -16
- package/dist/createHotUpdaterCore.d.cts +10 -3
- package/dist/createHotUpdaterCore.d.mts +10 -3
- package/dist/createHotUpdaterCore.mjs +11 -13
- package/dist/database/aggregateBatching.cjs +626 -0
- package/dist/database/aggregateBatching.d.cts +10 -0
- package/dist/database/aggregateBatching.d.mts +10 -0
- package/dist/database/aggregateBatching.mjs +625 -0
- package/dist/{insights/errors.cjs → database/busy.cjs} +2 -15
- package/dist/database/busy.d.cts +12 -0
- package/dist/database/busy.d.mts +12 -0
- package/dist/{insights/errors.mjs → database/busy.mjs} +3 -14
- package/dist/database/database.cjs +22 -3
- package/dist/database/database.d.cts +15 -2
- package/dist/database/database.d.mts +15 -2
- package/dist/database/database.mjs +22 -3
- package/dist/database/definitions.d.cts +10 -0
- package/dist/database/definitions.d.mts +10 -0
- package/dist/database/engine.d.cts +2 -1
- package/dist/database/engine.d.mts +2 -1
- package/dist/database/engineAggregates.cjs +7 -4
- package/dist/database/engineAggregates.mjs +8 -5
- package/dist/database/engineReads.cjs +5 -3
- package/dist/database/engineReads.mjs +6 -4
- package/dist/database/engineTransaction.cjs +2 -6
- package/dist/database/engineTransaction.mjs +2 -6
- package/dist/database/fence.cjs +2 -1
- package/dist/database/fence.mjs +2 -1
- package/dist/database/index.cjs +17 -8
- package/dist/database/index.d.cts +5 -4
- package/dist/database/index.d.mts +5 -4
- package/dist/database/index.mjs +6 -5
- package/dist/database/kv/kvAdapter.cjs +15 -3
- package/dist/database/kv/kvAdapter.d.cts +8 -0
- package/dist/database/kv/kvAdapter.d.mts +8 -0
- package/dist/database/kv/kvAdapter.mjs +16 -4
- package/dist/database/resolveSchema.cjs +4 -1
- package/dist/database/resolveSchema.d.cts +1 -1
- package/dist/database/resolveSchema.d.mts +1 -1
- package/dist/database/resolveSchema.mjs +4 -1
- package/dist/database/retention.cjs +92 -0
- package/dist/database/retention.mjs +90 -0
- package/dist/database/schema.cjs +5 -2
- package/dist/database/schema.d.cts +30 -1
- package/dist/database/schema.d.mts +30 -1
- package/dist/database/schema.mjs +5 -2
- package/dist/database/sql/sqlAdapter.cjs +8 -0
- package/dist/database/sql/sqlAdapter.mjs +9 -1
- package/dist/database/sql/sqlSchema.cjs +56 -0
- package/dist/database/sql/sqlSchema.mjs +55 -1
- package/dist/db/coreDatabase.cjs +85 -0
- package/dist/db/coreDatabase.d.cts +57 -0
- package/dist/db/coreDatabase.d.mts +57 -0
- package/dist/db/coreDatabase.mjs +78 -0
- package/dist/db/index.cjs +11 -3
- package/dist/db/index.d.cts +7 -3
- package/dist/db/index.d.mts +7 -3
- package/dist/db/index.mjs +6 -3
- package/dist/db/types.d.cts +3 -3
- package/dist/db/types.d.mts +3 -3
- package/dist/diff.cjs +3 -0
- package/dist/diff.d.cts +2 -0
- package/dist/diff.d.mts +2 -0
- package/dist/diff.mjs +2 -0
- package/dist/handler.cjs +2 -14
- package/dist/handler.mjs +1 -13
- package/dist/handlerReleaseCatalogRoutes.cjs +21 -2
- package/dist/handlerReleaseCatalogRoutes.mjs +21 -2
- package/dist/index.cjs +2 -6
- package/dist/index.d.cts +4 -7
- package/dist/index.d.mts +4 -7
- package/dist/index.mjs +2 -4
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/{apiKeys.cjs → plugins/api-keys/apiKeys.cjs} +1 -1
- package/dist/{apiKeys.d.cts → plugins/api-keys/apiKeys.d.cts} +2 -2
- package/dist/{apiKeys.d.mts → plugins/api-keys/apiKeys.d.mts} +2 -2
- package/dist/{apiKeys.mjs → plugins/api-keys/apiKeys.mjs} +1 -1
- package/dist/plugins/api-keys/cli.cjs +115 -0
- package/dist/plugins/api-keys/cli.mjs +114 -0
- package/dist/plugins/api-keys/index.cjs +9 -6
- package/dist/plugins/api-keys/index.d.cts +4 -5
- package/dist/plugins/api-keys/index.d.mts +4 -5
- package/dist/plugins/api-keys/index.mjs +9 -7
- package/dist/plugins/api-keys/model.d.cts +20 -2
- package/dist/plugins/api-keys/model.d.mts +20 -2
- package/dist/plugins/definePlugin.cjs +1 -1
- package/dist/plugins/definePlugin.d.cts +130 -3
- package/dist/plugins/definePlugin.d.mts +130 -3
- package/dist/plugins/definePlugin.mjs +1 -1
- package/dist/plugins/index.cjs +25 -4
- package/dist/plugins/index.d.cts +7 -4
- package/dist/plugins/index.d.mts +7 -4
- package/dist/plugins/index.mjs +6 -4
- package/dist/plugins/insights/contract.cjs +193 -0
- package/dist/plugins/insights/contract.mjs +189 -0
- package/dist/{insights → plugins/insights}/domain.d.cts +60 -15
- package/dist/{insights → plugins/insights}/domain.d.mts +60 -15
- package/dist/plugins/insights/errors.cjs +15 -0
- package/dist/plugins/insights/errors.mjs +14 -0
- package/dist/{insights → plugins/insights}/eventInput.cjs +155 -22
- package/dist/{insights → plugins/insights}/eventInput.mjs +155 -22
- package/dist/plugins/insights/eventRow.cjs +49 -0
- package/dist/plugins/insights/eventRow.d.cts +64 -0
- package/dist/plugins/insights/eventRow.d.mts +64 -0
- package/dist/plugins/insights/eventRow.mjs +47 -0
- package/dist/plugins/insights/index.cjs +106 -26
- package/dist/plugins/insights/index.d.cts +130 -27
- package/dist/plugins/insights/index.d.mts +130 -27
- package/dist/plugins/insights/index.mjs +106 -29
- package/dist/plugins/insights/model.cjs +2 -2
- package/dist/plugins/insights/model.d.cts +1 -1
- package/dist/plugins/insights/model.d.mts +1 -1
- package/dist/plugins/insights/model.mjs +1 -1
- package/dist/plugins/insights/modelTypes.d.cts +176 -0
- package/dist/plugins/insights/modelTypes.d.mts +176 -0
- package/dist/plugins/insights/overview.cjs +109 -0
- package/dist/plugins/insights/overview.d.cts +15 -0
- package/dist/plugins/insights/overview.d.mts +15 -0
- package/dist/plugins/insights/overview.mjs +107 -0
- package/dist/{insights → plugins/insights}/provider.cjs +33 -14
- package/dist/{insights → plugins/insights}/provider.d.cts +2 -2
- package/dist/{insights → plugins/insights}/provider.d.mts +2 -2
- package/dist/{insights → plugins/insights}/provider.mjs +30 -11
- package/dist/{insights → plugins/insights}/queryInput.cjs +2 -2
- package/dist/{insights → plugins/insights}/queryInput.mjs +2 -2
- package/dist/plugins/insights/reads.cjs +298 -88
- package/dist/plugins/insights/reads.d.cts +61 -0
- package/dist/plugins/insights/reads.d.mts +61 -0
- package/dist/plugins/insights/reads.mjs +298 -90
- package/dist/plugins/insights/recordEvent.cjs +223 -39
- package/dist/plugins/insights/recordEvent.d.cts +9 -4
- package/dist/plugins/insights/recordEvent.d.mts +9 -4
- package/dist/plugins/insights/recordEvent.mjs +221 -37
- package/dist/{insights → plugins/insights}/routes.cjs +3 -30
- package/dist/{insights → plugins/insights}/routes.mjs +4 -28
- package/dist/plugins/insights/schema.cjs +182 -40
- package/dist/plugins/insights/schema.d.cts +446 -6
- package/dist/plugins/insights/schema.d.mts +446 -6
- package/dist/plugins/insights/schema.mjs +180 -41
- package/dist/plugins/insights/testing/eventRow.d.mts +64 -0
- package/dist/plugins/insights/testing/expectInsightsIndex.mjs +18 -0
- package/dist/plugins/insights/testing/fixtures.d.mts +8 -0
- package/dist/plugins/insights/testing/fixtures.mjs +25 -0
- package/dist/plugins/insights/testing/index.d.mts +24 -0
- package/dist/plugins/insights/testing/index.mjs +25 -0
- package/dist/plugins/insights/testing/insightsModelTests.mjs +714 -0
- package/dist/plugins/insights/testing/modelTypes.d.mts +176 -0
- package/dist/plugins/insights/testing/setupInsightsHttpTestSuite.d.mts +11 -0
- package/dist/plugins/insights/testing/setupInsightsHttpTestSuite.mjs +107 -0
- package/dist/plugins/insights/testing/setupInsightsModelTestSuite.d.mts +11 -0
- package/dist/plugins/insights/testing/setupInsightsModelTestSuite.mjs +31 -0
- package/dist/plugins/insights/testing/types.d.mts +26 -0
- package/dist/{insights → plugins/insights}/types.d.cts +3 -2
- package/dist/{insights → plugins/insights}/types.d.mts +3 -2
- package/package.json +18 -7
- package/src/adapters/drizzle.conformance.integration.spec.ts +2 -2
- package/src/adapters/drizzle.readBudgets.integration.spec.ts +6 -4
- package/src/adapters/drizzle.spec.ts +15 -7
- package/src/adapters/drizzle.ts +6 -6
- package/src/adapters/kysely.conformance.integration.spec.ts +2 -2
- package/src/adapters/kysely.readBudgets.integration.spec.ts +4 -2
- package/src/adapters/kysely.spec.ts +13 -9
- package/src/adapters/kysely.ts +2 -2
- package/src/adapters/mongodb.integration.spec.ts +98 -7
- package/src/adapters/mongodb.ts +2 -2
- package/src/adapters/mongodbAdapter.ts +14 -0
- package/src/adapters/prisma.conformance.integration.spec.ts +2 -2
- package/src/adapters/prisma.integration.spec.ts +16 -8
- package/src/adapters/prisma.readBudgets.integration.spec.ts +5 -2
- package/src/adapters/prisma.spec.ts +17 -9
- package/src/adapters/prisma.ts +6 -6
- package/src/adapters/prismaTestClients.ts +8 -1
- package/src/assembly/assemblePlugins.ts +73 -40
- package/src/assembly/assembly.spec.ts +63 -11
- package/src/assembly/assembly.types.spec.ts +50 -2
- package/src/assembly/clientPlugins.ts +113 -0
- package/src/assembly/configError.ts +4 -0
- package/src/assembly/databasePlugins.ts +11 -5
- package/src/assembly/pluginCli.spec.ts +254 -0
- package/src/assembly/pluginCli.ts +248 -0
- package/src/assembly/{builtInRoutes.spec.ts → pluginRoutes.spec.ts} +10 -37
- package/src/createHotUpdaterCore.ts +21 -17
- package/src/database/aggregateBatching.spec.ts +577 -0
- package/src/database/aggregateBatching.ts +803 -0
- package/src/{insights/errors.spec.ts → database/busy.spec.ts} +2 -5
- package/src/{insights/errors.ts → database/busy.ts} +1 -13
- package/src/database/database.ts +39 -8
- package/src/database/definitions.ts +4 -0
- package/src/database/engineAggregates.spec.ts +4 -7
- package/src/database/engineAggregates.ts +5 -9
- package/src/database/engineReads.ts +7 -5
- package/src/database/engineTransaction.ts +1 -7
- package/src/database/fence.spec.ts +5 -5
- package/src/database/fence.ts +4 -0
- package/src/database/index.ts +13 -7
- package/src/database/kv/kvAdapter.batching.integration.spec.ts +15 -0
- package/src/database/kv/kvAdapter.conformance.integration.spec.ts +2 -0
- package/src/database/kv/kvAdapter.spec.ts +128 -1
- package/src/database/kv/kvAdapter.ts +30 -1
- package/src/database/kv/kvTestStore.ts +28 -1
- package/src/database/resolveSchema.ts +4 -1
- package/src/database/retention.spec.ts +314 -0
- package/src/database/retention.ts +129 -0
- package/src/database/schema.ts +44 -0
- package/src/database/sql/sqlAdapter.integration.spec.ts +4 -7
- package/src/database/sql/sqlAdapter.readBudgets.integration.spec.ts +18 -4
- package/src/database/sql/sqlAdapter.spec.ts +91 -3
- package/src/database/sql/sqlAdapter.ts +20 -0
- package/src/database/sql/sqlSchema.ts +81 -0
- package/src/databaseTestSuite.spec.ts +9 -4
- package/src/db/coreDatabase.ts +153 -0
- package/src/db/engineSql.spec.ts +8 -7
- package/src/db/index.ts +19 -2
- package/src/db/pluginTables.spec.ts +57 -27
- package/src/db/types.ts +3 -3
- package/src/diff.ts +6 -0
- package/src/handler-standalone.integration.spec.ts +2 -3
- package/src/handler.ts +1 -32
- package/src/handlerReleaseCatalogRoutes.ts +26 -2
- package/src/index.ts +0 -10
- package/src/{apiKeys.integration.spec.ts → plugins/api-keys/apiKeys.integration.spec.ts} +4 -4
- package/src/{apiKeys.spec.ts → plugins/api-keys/apiKeys.spec.ts} +3 -3
- package/src/{apiKeys.ts → plugins/api-keys/apiKeys.ts} +1 -1
- package/src/plugins/api-keys/apiKeysPlugin.spec.ts +11 -15
- package/src/plugins/api-keys/cli.spec.ts +187 -0
- package/src/plugins/api-keys/cli.ts +140 -0
- package/src/plugins/api-keys/index.ts +48 -41
- package/src/plugins/api-keys/model.ts +21 -2
- package/src/plugins/definePlugin.ts +144 -2
- package/src/plugins/index.ts +20 -2
- package/src/plugins/insights/contract.spec.ts +285 -0
- package/src/plugins/insights/contract.ts +372 -0
- package/src/{insights → plugins/insights}/domain.ts +73 -15
- package/src/plugins/insights/errors.ts +11 -0
- package/src/{insights → plugins/insights}/eventInput.ts +189 -29
- package/src/plugins/insights/eventRow.ts +209 -0
- package/src/plugins/insights/index.ts +181 -45
- package/src/{insights → plugins/insights}/insights.integration.spec.ts +330 -34
- package/src/plugins/insights/insightsRollout.integration.spec.ts +1 -2
- package/src/plugins/insights/model.spec.ts +3 -5
- package/src/plugins/insights/model.ts +2 -3
- package/src/plugins/insights/modelTypes.ts +197 -0
- package/src/plugins/insights/overview.spec.ts +64 -0
- package/src/plugins/insights/overview.ts +214 -0
- package/src/{insights → plugins/insights}/provider.spec.ts +21 -10
- package/src/{insights → plugins/insights}/provider.ts +58 -21
- package/src/{insights → plugins/insights}/queryInput.ts +2 -2
- package/src/plugins/insights/reads.spec.ts +340 -52
- package/src/plugins/insights/reads.ts +453 -123
- package/src/plugins/insights/recordEvent.spec.ts +474 -20
- package/src/plugins/insights/recordEvent.ts +301 -63
- package/src/plugins/insights/retention.spec.ts +115 -0
- package/src/{insights → plugins/insights}/routes.ts +1 -31
- package/src/plugins/insights/schema.ts +356 -176
- package/src/plugins/insights/testing/expectInsightsIndex.spec.ts +42 -0
- package/src/plugins/insights/testing/expectInsightsIndex.ts +20 -0
- package/src/plugins/insights/testing/fixtures.ts +30 -0
- package/src/plugins/insights/testing/index.ts +45 -0
- package/src/plugins/insights/testing/insightsModelTests.ts +860 -0
- package/src/plugins/insights/testing/setupInsightsHttpTestSuite.ts +152 -0
- package/src/plugins/insights/testing/setupInsightsModelTestSuite.ts +36 -0
- package/src/plugins/insights/testing/types.ts +30 -0
- package/src/{insights → plugins/insights}/types.ts +1 -1
- package/src/readBudgets.testFixtures.ts +2 -2
- package/src/releaseCatalogRoutes.integration.spec.ts +7 -0
- package/src/retention.spec.ts +73 -0
- package/src/runtime.spec.ts +11 -12
- package/src/runtime.testFixtures.ts +7 -2
- package/src/testUtilsMutation.integration.spec.ts +2 -1
- package/src/testUtilsPackage.integration.spec.ts +7 -3
- package/dist/db/builtInDatabase.cjs +0 -104
- package/dist/db/builtInDatabase.d.cts +0 -38
- package/dist/db/builtInDatabase.d.mts +0 -38
- package/dist/db/builtInDatabase.mjs +0 -97
- package/dist/plugins/builtIn.cjs +0 -7
- package/dist/plugins/builtIn.mjs +0 -6
- package/src/db/builtInDatabase.ts +0 -149
- package/src/plugins/builtIn.ts +0 -5
|
@@ -40,6 +40,8 @@ const ready = async (connect: () => Promise<unknown>) => {
|
|
|
40
40
|
}
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
const MYSQL_URI = "mysql://root:hot_updater@127.0.0.1:53306/hot_updater";
|
|
44
|
+
|
|
43
45
|
let postgres: pg.Pool;
|
|
44
46
|
let mariadb: mysql.Pool;
|
|
45
47
|
|
|
@@ -50,11 +52,23 @@ beforeAll(async () => {
|
|
|
50
52
|
"postgres://postgres:hot_updater@127.0.0.1:55432/hot_updater",
|
|
51
53
|
max: 16,
|
|
52
54
|
});
|
|
53
|
-
mariadb = mysql.createPool({
|
|
54
|
-
uri: "mysql://root:hot_updater@127.0.0.1:53306/hot_updater",
|
|
55
|
-
connectionLimit: 16,
|
|
56
|
-
});
|
|
57
55
|
await ready(() => postgres.query("SELECT 1"));
|
|
56
|
+
// Each row a plan evaluates costs as much as an index entry it reads, as
|
|
57
|
+
// PostgreSQL's planner settings make it, so a small table plans like a
|
|
58
|
+
// production-sized one: an index range rather than a primary-key prefix
|
|
59
|
+
// filtered down to it. Sessions opened after the flush use the new cost.
|
|
60
|
+
await ready(async () => {
|
|
61
|
+
const admin = await mysql.createConnection(MYSQL_URI);
|
|
62
|
+
try {
|
|
63
|
+
await admin.query(
|
|
64
|
+
"UPDATE mysql.server_cost SET cost_value = 1 WHERE cost_name = 'row_evaluate_cost'",
|
|
65
|
+
);
|
|
66
|
+
await admin.query("FLUSH OPTIMIZER_COSTS");
|
|
67
|
+
} finally {
|
|
68
|
+
await admin.end();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
mariadb = mysql.createPool({ uri: MYSQL_URI, connectionLimit: 16 });
|
|
58
72
|
await ready(() => mariadb.query("SELECT 1"));
|
|
59
73
|
}, 180_000);
|
|
60
74
|
|
|
@@ -4,12 +4,13 @@ import { PGlite } from "@electric-sql/pglite";
|
|
|
4
4
|
import type { PhysicalTable } from "@hot-updater/plugin-core/internal";
|
|
5
5
|
import {
|
|
6
6
|
conformanceCounters,
|
|
7
|
+
conformanceExpiring,
|
|
7
8
|
conformanceItems,
|
|
8
9
|
setupDatabaseAdapterConformanceSuite,
|
|
9
10
|
} from "@hot-updater/test-utils";
|
|
10
11
|
import { afterAll, describe, expect, it } from "vitest";
|
|
11
12
|
|
|
12
|
-
import {
|
|
13
|
+
import { coreSchema } from "../../db/coreDatabase";
|
|
13
14
|
import {
|
|
14
15
|
classifySqlError,
|
|
15
16
|
createSqlAdapter,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
type SqlStatement,
|
|
19
20
|
WRITE_GUARD_TABLE,
|
|
20
21
|
} from "./sqlAdapter";
|
|
22
|
+
import { pruneStatements } from "./sqlSchema";
|
|
21
23
|
import {
|
|
22
24
|
pgliteBatchExecutor,
|
|
23
25
|
pgliteExecutor,
|
|
@@ -145,9 +147,9 @@ describe("sql core", () => {
|
|
|
145
147
|
expect(() => createTableStatements("mysql", [catalogs(false)])).toThrow(
|
|
146
148
|
"catalogs key needs 8192 bytes on MySQL, over its 3072",
|
|
147
149
|
);
|
|
148
|
-
// every table the
|
|
150
|
+
// every core table fits; the plugins' tables are checked beside them
|
|
149
151
|
expect(() =>
|
|
150
|
-
createTableStatements("mysql",
|
|
152
|
+
createTableStatements("mysql", coreSchema.tables),
|
|
151
153
|
).not.toThrow();
|
|
152
154
|
});
|
|
153
155
|
|
|
@@ -236,6 +238,92 @@ describe("sql core", () => {
|
|
|
236
238
|
]);
|
|
237
239
|
});
|
|
238
240
|
|
|
241
|
+
it("indexes a retention column that no index or key already leads with", () => {
|
|
242
|
+
const index = (table: PhysicalTable) =>
|
|
243
|
+
createTableStatements("postgresql", [table]).filter((statement) =>
|
|
244
|
+
statement.includes("__retention"),
|
|
245
|
+
);
|
|
246
|
+
expect(index(conformanceExpiring)).toEqual([
|
|
247
|
+
'CREATE INDEX IF NOT EXISTS "conformance_expiring__retention" ON "conformance_expiring" ("at", "id")',
|
|
248
|
+
]);
|
|
249
|
+
// An index or a key that starts with the column serves a prune already.
|
|
250
|
+
const led = (retention: PhysicalTable["retention"]) =>
|
|
251
|
+
index({ ...conformanceCounters, retention });
|
|
252
|
+
expect(led({ column: "scope", ms: 1 })).toEqual([]);
|
|
253
|
+
expect(
|
|
254
|
+
index({
|
|
255
|
+
...conformanceExpiring,
|
|
256
|
+
indexes: [{ name: "byAt", eq: ["at"], sort: [] }],
|
|
257
|
+
}),
|
|
258
|
+
).toEqual([]);
|
|
259
|
+
expect(led({ column: "hits", ms: 1 })).toEqual([
|
|
260
|
+
'CREATE INDEX IF NOT EXISTS "conformance_counters__retention" ON "conformance_counters" ("hits", "scope", "shard")',
|
|
261
|
+
]);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("walks expired rows in the order of the index that leads with the retention column", () => {
|
|
265
|
+
const events: PhysicalTable = {
|
|
266
|
+
...conformanceExpiring,
|
|
267
|
+
columns: [
|
|
268
|
+
...conformanceExpiring.columns,
|
|
269
|
+
{ name: "received", type: "integer", nullable: false },
|
|
270
|
+
],
|
|
271
|
+
indexes: [{ name: "byAt", eq: ["at"], sort: ["received"] }],
|
|
272
|
+
};
|
|
273
|
+
const [postgres] = pruneStatements("postgresql", events, "", 10).map(
|
|
274
|
+
(render) => render(() => "$1"),
|
|
275
|
+
);
|
|
276
|
+
expect(postgres).toBe(
|
|
277
|
+
'DELETE FROM "conformance_expiring" WHERE "conformance_expiring"."at" <= $1 AND ("conformance_expiring"."at", "conformance_expiring"."received", "conformance_expiring"."id") <= (SELECT b."at", b."received", b."id" FROM (SELECT i."at", i."received", i."id" FROM "conformance_expiring" i WHERE i."at" <= $1 ORDER BY i."at" ASC, i."received" ASC, i."id" ASC LIMIT 10) b ORDER BY b."at" DESC, b."received" DESC, b."id" DESC LIMIT 1)',
|
|
278
|
+
);
|
|
279
|
+
const [entries, rows] = pruneStatements(
|
|
280
|
+
"mysql",
|
|
281
|
+
conformanceExpiring,
|
|
282
|
+
"",
|
|
283
|
+
10,
|
|
284
|
+
).map((render) => render(() => "?"));
|
|
285
|
+
expect(entries).toBe(
|
|
286
|
+
"DELETE e FROM `conformance_expiring__byTag` e JOIN (SELECT i.`at`, i.`id` FROM `conformance_expiring` i WHERE i.`at` <= ? ORDER BY i.`at` ASC, i.`id` ASC LIMIT 10) d ON e.`id` = d.`id`",
|
|
287
|
+
);
|
|
288
|
+
expect(rows).toBe(
|
|
289
|
+
"DELETE FROM `conformance_expiring` WHERE `at` <= ? ORDER BY `at`, `id` LIMIT 10",
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it.each(["PGlite", "SQLite"] as const)(
|
|
294
|
+
"prunes expired rows with their index table entries on %s",
|
|
295
|
+
async (engine) => {
|
|
296
|
+
const sqlite = engine === "SQLite" ? new DatabaseSync(":memory:") : null;
|
|
297
|
+
tests += 1;
|
|
298
|
+
const executor = sqlite ? sqliteExecutor(sqlite) : pgliteExecutor(pglite);
|
|
299
|
+
const prefix = sqlite ? "" : `t${tests}_`;
|
|
300
|
+
const adapter = createSqlAdapter({ executor, tablePrefix: prefix });
|
|
301
|
+
await adapter.migrations!.apply([conformanceExpiring]);
|
|
302
|
+
await adapter.write(
|
|
303
|
+
[5, 10, 20, null].map((at, position) => ({
|
|
304
|
+
type: "insert",
|
|
305
|
+
table: conformanceExpiring,
|
|
306
|
+
row: { id: `r${position}`, grp: "g", at, tags: ["a", "b"], _v: 0 },
|
|
307
|
+
})),
|
|
308
|
+
);
|
|
309
|
+
const count = async (name: string) =>
|
|
310
|
+
Number(
|
|
311
|
+
(
|
|
312
|
+
await executor.execute({
|
|
313
|
+
sql: `SELECT COUNT(*) AS n FROM "${prefix}${name}"`,
|
|
314
|
+
params: [],
|
|
315
|
+
})
|
|
316
|
+
).rows[0]!.n,
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
expect(await adapter.prune!(conformanceExpiring, 10, 500)).toBe(2);
|
|
320
|
+
expect(await count("conformance_expiring")).toBe(2);
|
|
321
|
+
// Each row had two tag entries; the pruned rows' went with them.
|
|
322
|
+
expect(await count("conformance_expiring__byTag")).toBe(4);
|
|
323
|
+
sqlite?.close();
|
|
324
|
+
},
|
|
325
|
+
);
|
|
326
|
+
|
|
239
327
|
it("classifies constraint and transient driver errors", () => {
|
|
240
328
|
const withCode = (fields: Record<string, unknown>) =>
|
|
241
329
|
Object.assign(new Error("driver"), fields);
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
createTableStatements,
|
|
22
22
|
isMultiIndex,
|
|
23
|
+
pruneStatements,
|
|
23
24
|
quoteSql,
|
|
24
25
|
type SqlDialect,
|
|
25
26
|
} from "./sqlSchema";
|
|
@@ -515,6 +516,25 @@ export const createSqlAdapter = ({
|
|
|
515
516
|
}
|
|
516
517
|
},
|
|
517
518
|
|
|
519
|
+
async prune(table, before, limit) {
|
|
520
|
+
const statements = pruneStatements(
|
|
521
|
+
dialect,
|
|
522
|
+
table,
|
|
523
|
+
tablePrefix,
|
|
524
|
+
limit,
|
|
525
|
+
).map((render) => sql((bind) => render(() => bind(before))));
|
|
526
|
+
const results = executor.batch
|
|
527
|
+
? await executor.batch(statements)
|
|
528
|
+
: await executor.transaction(async (connection) => {
|
|
529
|
+
const done: SqlResult[] = [];
|
|
530
|
+
for (const statement of statements) {
|
|
531
|
+
done.push(await connection.execute(statement));
|
|
532
|
+
}
|
|
533
|
+
return done;
|
|
534
|
+
});
|
|
535
|
+
return results.at(-1)?.changes ?? 0;
|
|
536
|
+
},
|
|
537
|
+
|
|
518
538
|
migrations: {
|
|
519
539
|
async apply(tables) {
|
|
520
540
|
// A batching executor's writes also need the write guard table.
|
|
@@ -155,6 +155,16 @@ export const sqlTableShapes = (
|
|
|
155
155
|
},
|
|
156
156
|
];
|
|
157
157
|
});
|
|
158
|
+
// A prune walks expired rows in an index's order, so one leads with the column.
|
|
159
|
+
const retention = table.retention && retentionOrder(table);
|
|
160
|
+
if (retention && !retention.indexed) {
|
|
161
|
+
indexes.push({
|
|
162
|
+
kind: "index",
|
|
163
|
+
name: shortSqlName(`${name}__retention`),
|
|
164
|
+
unique: false,
|
|
165
|
+
columns: retention.columns,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
158
168
|
return {
|
|
159
169
|
name,
|
|
160
170
|
columns: table.columns.map((column) => ({
|
|
@@ -203,3 +213,74 @@ export const createTableStatements = (
|
|
|
203
213
|
};
|
|
204
214
|
return sqlTableShapes(dialect, tables, tablePrefix).flatMap(render);
|
|
205
215
|
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The order a prune walks a table's expired rows in, which ends with the
|
|
219
|
+
* key so it is total: the columns of the first index that leads with the
|
|
220
|
+
* retention column, or the key when it does, or else the column and the
|
|
221
|
+
* key, which then need an index of their own (`indexed` false).
|
|
222
|
+
*/
|
|
223
|
+
export const retentionOrder = (table: PhysicalTable) => {
|
|
224
|
+
const { column } = table.retention!;
|
|
225
|
+
for (const index of table.indexes) {
|
|
226
|
+
const columns = [...index.eq, ...indexOrderColumns(table, index)];
|
|
227
|
+
if (!index.unique && !isMultiIndex(table, index) && columns[0] === column) {
|
|
228
|
+
return { columns, indexed: true };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (table.key[0] === column) return { columns: table.key, indexed: true };
|
|
232
|
+
const key = table.key.filter((field) => field !== column);
|
|
233
|
+
return { columns: [column, ...key], indexed: false };
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The statements that delete up to `limit` expired rows of a table with
|
|
238
|
+
* retention, oldest first in {@link retentionOrder}: its multi-valued index
|
|
239
|
+
* entries, then the rows, which a concurrent write that moved the retention
|
|
240
|
+
* column past `before` keeps. `before` renders the bound each time it
|
|
241
|
+
* appears. MySQL deletes with ORDER BY and LIMIT. Elsewhere the cutoff is
|
|
242
|
+
* the last of the `limit` oldest expired rows in that order, a row value
|
|
243
|
+
* the index ranges over, and the shapes pass Supabase's apply RPC: no IN, no
|
|
244
|
+
* AS, aliases `i` and `b`.
|
|
245
|
+
*/
|
|
246
|
+
export const pruneStatements = (
|
|
247
|
+
dialect: SqlDialect,
|
|
248
|
+
table: PhysicalTable,
|
|
249
|
+
tablePrefix: string,
|
|
250
|
+
limit: number,
|
|
251
|
+
): ((before: () => string) => string)[] => {
|
|
252
|
+
const quote = (name: string) => quoteSql(dialect, name);
|
|
253
|
+
const name = quote(`${tablePrefix}${table.name}`);
|
|
254
|
+
const at = quote(table.retention!.column);
|
|
255
|
+
const order = retentionOrder(table).columns.map(quote);
|
|
256
|
+
const list = (alias: string, direction = "") =>
|
|
257
|
+
order.map((column) => `${alias}.${column}${direction}`).join(", ");
|
|
258
|
+
const oldest = (before: () => string) =>
|
|
259
|
+
`SELECT ${list("i")} FROM ${name} i WHERE i.${at} <= ${before()} ORDER BY ${list("i", " ASC")} LIMIT ${limit}`;
|
|
260
|
+
const doomed = (alias: string, before: () => string) =>
|
|
261
|
+
`${alias}.${at} <= ${before()} AND (${list(alias)}) <= (SELECT ${list("b")} FROM (${oldest(before)}) b ORDER BY ${list("b", " DESC")} LIMIT 1)`;
|
|
262
|
+
const entries = table.indexes
|
|
263
|
+
.filter((index) => isMultiIndex(table, index))
|
|
264
|
+
.map((index) => quote(`${tablePrefix}${table.name}__${index.name}`));
|
|
265
|
+
const keyOf = (left: string, right: string) =>
|
|
266
|
+
table.key
|
|
267
|
+
.map((column) => `${left}.${quote(column)} = ${right}.${quote(column)}`)
|
|
268
|
+
.join(" AND ");
|
|
269
|
+
if (dialect === "mysql") {
|
|
270
|
+
return [
|
|
271
|
+
...entries.map(
|
|
272
|
+
(index) => (before: () => string) =>
|
|
273
|
+
`DELETE e FROM ${index} e JOIN (${oldest(before)}) d ON ${keyOf("e", "d")}`,
|
|
274
|
+
),
|
|
275
|
+
(before) =>
|
|
276
|
+
`DELETE FROM ${name} WHERE ${at} <= ${before()} ORDER BY ${order.join(", ")} LIMIT ${limit}`,
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
return [
|
|
280
|
+
...entries.map(
|
|
281
|
+
(index) => (before: () => string) =>
|
|
282
|
+
`DELETE FROM ${index} WHERE EXISTS (SELECT * FROM ${name} b WHERE ${keyOf("b", index)} AND ${doomed("b", before)})`,
|
|
283
|
+
),
|
|
284
|
+
(before) => `DELETE FROM ${name} WHERE ${doomed(name, before)}`,
|
|
285
|
+
];
|
|
286
|
+
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { createDatabasePluginApis } from "./assembly/databasePlugins";
|
|
8
8
|
import { createHotUpdater } from "./index";
|
|
9
9
|
import { createInsightsModel, insights } from "./plugins/insights";
|
|
10
|
+
import { insightsTestSuite } from "./plugins/insights/testing";
|
|
10
11
|
|
|
11
12
|
let adapter = createMemoryAdapter();
|
|
12
13
|
|
|
@@ -32,8 +33,12 @@ setupDatabaseTestSuite({
|
|
|
32
33
|
clientAccess: "public",
|
|
33
34
|
}).handlers,
|
|
34
35
|
),
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
plugins: [
|
|
37
|
+
insightsTestSuite({
|
|
38
|
+
createModel: (database) =>
|
|
39
|
+
createInsightsModel(
|
|
40
|
+
createDatabasePluginApis(database, [insights()]).insights,
|
|
41
|
+
),
|
|
42
|
+
}),
|
|
43
|
+
],
|
|
39
44
|
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { AggregateBatching } from "@hot-updater/plugin-core";
|
|
2
|
+
import type { DatabaseAdapter } from "@hot-updater/plugin-core/internal";
|
|
3
|
+
|
|
4
|
+
import { coreModule, HOT_UPDATER_SCHEMA_VERSION } from "../core/schema";
|
|
5
|
+
import type { ModelShape } from "../database/definitions";
|
|
6
|
+
import {
|
|
7
|
+
ENGINE_SCHEMA_KEY,
|
|
8
|
+
ENGINE_SCHEMA_VERSION,
|
|
9
|
+
isMissingSchemaError,
|
|
10
|
+
readSettings,
|
|
11
|
+
withSchemaFence,
|
|
12
|
+
type SchemaSettings,
|
|
13
|
+
} from "../database/fence";
|
|
14
|
+
import { resolveSchema } from "../database/resolveSchema";
|
|
15
|
+
import { createEngineMigrator } from "./engineMigrator";
|
|
16
|
+
import { migrateSchema } from "./schemaSettings";
|
|
17
|
+
import type { ToolingDatabase, ToolingTarget } from "./types";
|
|
18
|
+
|
|
19
|
+
/** Core's tables: the tables every provider's tooling creates. */
|
|
20
|
+
export const coreSchema = resolveSchema([coreModule]);
|
|
21
|
+
|
|
22
|
+
/** The settings rows a provider's database is fenced by. */
|
|
23
|
+
export const coreSettings: SchemaSettings = {
|
|
24
|
+
[ENGINE_SCHEMA_KEY]: ENGINE_SCHEMA_VERSION,
|
|
25
|
+
"schema.core": HOT_UPDATER_SCHEMA_VERSION,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** What `hot-updater db` creates for a server without plugins. */
|
|
29
|
+
export const coreTarget: ToolingTarget = {
|
|
30
|
+
schema: coreSchema,
|
|
31
|
+
settings: coreSettings,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** A plugin as the tooling reads it: its id, its tables, and their version. */
|
|
35
|
+
export interface PluginTables {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly schemaVersion: string;
|
|
38
|
+
readonly schema: Readonly<Record<string, ModelShape>>;
|
|
39
|
+
/** `false` keeps the declared table names instead of prefixing them with the id. */
|
|
40
|
+
readonly namespace?: false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Each plugin's `schema.<id>` settings row. */
|
|
44
|
+
export const pluginSettings = (
|
|
45
|
+
plugins: readonly PluginTables[],
|
|
46
|
+
): SchemaSettings =>
|
|
47
|
+
Object.fromEntries(
|
|
48
|
+
plugins.map(({ id, schemaVersion }) => [`schema.${id}`, schemaVersion]),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
/** A plugin's schema as the engine resolves it: prefixed by its id unless it keeps its names. */
|
|
52
|
+
export const pluginModule = <TSchema>({
|
|
53
|
+
id,
|
|
54
|
+
schema,
|
|
55
|
+
namespace,
|
|
56
|
+
}: {
|
|
57
|
+
readonly id: string;
|
|
58
|
+
readonly schema: TSchema;
|
|
59
|
+
readonly namespace?: false;
|
|
60
|
+
}) => ({
|
|
61
|
+
id,
|
|
62
|
+
schema,
|
|
63
|
+
...(namespace === false ? {} : { namespace: id }),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* What `hot-updater db` creates for a server's plugins: core's tables, then
|
|
68
|
+
* each plugin's tables with its `schema.<id>` settings row.
|
|
69
|
+
*/
|
|
70
|
+
export const toolingTargetOf = (
|
|
71
|
+
plugins: readonly PluginTables[],
|
|
72
|
+
): ToolingTarget =>
|
|
73
|
+
plugins.length === 0
|
|
74
|
+
? coreTarget
|
|
75
|
+
: {
|
|
76
|
+
schema: resolveSchema([coreModule, ...plugins.map(pluginModule)]),
|
|
77
|
+
settings: { ...coreSettings, ...pluginSettings(plugins) },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Creates core's tables and those of `plugins`, then writes their settings
|
|
82
|
+
* rows: what a provider's setup runs for the plugins its server runs.
|
|
83
|
+
*/
|
|
84
|
+
export const migrateCoreSchema = (
|
|
85
|
+
adapter: DatabaseAdapter,
|
|
86
|
+
name: string,
|
|
87
|
+
plugins: readonly PluginTables[] = [],
|
|
88
|
+
) => {
|
|
89
|
+
const { schema, settings } = toolingTargetOf(plugins);
|
|
90
|
+
return migrateSchema(adapter, name, schema.tables, settings);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export interface EngineDatabaseOptions {
|
|
94
|
+
readonly name: string;
|
|
95
|
+
readonly adapter: DatabaseAdapter;
|
|
96
|
+
/**
|
|
97
|
+
* Purges a CDN's copies of the cacheable client routes. Core calls it after
|
|
98
|
+
* a committed write that changes what those routes answer, since only core
|
|
99
|
+
* knows which of its tables they read.
|
|
100
|
+
*/
|
|
101
|
+
readonly onCachedRoutesChange?: () => Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Batches aggregates declared `batched`, for stores that bill each write;
|
|
104
|
+
* `false` or absent commits them with each transaction.
|
|
105
|
+
*/
|
|
106
|
+
readonly aggregateBatching?: AggregateBatching | false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A provider's database on the storage engine: its adapter behind the schema
|
|
111
|
+
* fence, which checks core's settings rows before the first read or write
|
|
112
|
+
* and names `hot-updater db migrate` when they are missing or stale. When the
|
|
113
|
+
* adapter creates its own tables, `hot-updater db migrate` creates core's
|
|
114
|
+
* tables and the server's plugin tables, then writes their settings rows.
|
|
115
|
+
*/
|
|
116
|
+
export const createEngineDatabase = ({
|
|
117
|
+
name,
|
|
118
|
+
adapter,
|
|
119
|
+
onCachedRoutesChange,
|
|
120
|
+
aggregateBatching,
|
|
121
|
+
}: EngineDatabaseOptions): ToolingDatabase => {
|
|
122
|
+
const fenced = withSchemaFence(adapter, name, coreSettings);
|
|
123
|
+
const createMigrator = ({ schema, settings } = coreTarget) =>
|
|
124
|
+
createEngineMigrator({
|
|
125
|
+
adapterName: name,
|
|
126
|
+
adapter,
|
|
127
|
+
schema,
|
|
128
|
+
settings,
|
|
129
|
+
readSettings: async () => {
|
|
130
|
+
const rows = await readSettings(adapter, Object.keys(settings)).catch(
|
|
131
|
+
(error: unknown) => {
|
|
132
|
+
if (isMissingSchemaError(error)) return [];
|
|
133
|
+
throw error;
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
return new Map(
|
|
137
|
+
rows.flatMap((row) =>
|
|
138
|
+
row === null ? [] : [[String(row.key), row.value]],
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
name,
|
|
145
|
+
adapter: fenced,
|
|
146
|
+
...(onCachedRoutesChange === undefined ? {} : { onCachedRoutesChange }),
|
|
147
|
+
...(aggregateBatching ? { aggregateBatching } : {}),
|
|
148
|
+
...(adapter.dispose === undefined
|
|
149
|
+
? {}
|
|
150
|
+
: { dispose: () => adapter.dispose!() }),
|
|
151
|
+
...(adapter.migrations === undefined ? {} : { createMigrator }),
|
|
152
|
+
};
|
|
153
|
+
};
|
package/src/db/engineSql.spec.ts
CHANGED
|
@@ -15,15 +15,16 @@ import {
|
|
|
15
15
|
pgliteExecutor,
|
|
16
16
|
sqliteExecutor,
|
|
17
17
|
} from "../database/sql/sqlTestExecutors";
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
createEngineDatabase,
|
|
22
|
-
} from "./builtInDatabase";
|
|
18
|
+
import { apiKeys } from "../plugins/api-keys";
|
|
19
|
+
import { insights } from "../plugins/insights";
|
|
20
|
+
import { createEngineDatabase, toolingTargetOf } from "./coreDatabase";
|
|
23
21
|
import { generateEngineSql } from "./engineSql";
|
|
24
22
|
|
|
23
|
+
/** Core's tables and the Insights and API keys plugins'. */
|
|
24
|
+
const target = toolingTargetOf([insights(), apiKeys()]);
|
|
25
|
+
|
|
25
26
|
const sql = (dialect: "postgresql" | "mysql" | "sqlite") =>
|
|
26
|
-
generateEngineSql(dialect,
|
|
27
|
+
generateEngineSql(dialect, target.schema, target.settings);
|
|
27
28
|
|
|
28
29
|
describe("the shared SQL schema", () => {
|
|
29
30
|
it("defaults engine columns, has no foreign keys, and writes the settings last", () => {
|
|
@@ -53,7 +54,7 @@ describe("the shared SQL schema", () => {
|
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
it("describes each table as its DDL creates it, for ORM schema generators", () => {
|
|
56
|
-
const tables =
|
|
57
|
+
const tables = target.schema.tables;
|
|
57
58
|
const names = (shapes: readonly SqlTableShape[]): string[] =>
|
|
58
59
|
shapes.flatMap((shape) => [
|
|
59
60
|
shape.name,
|
package/src/db/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
getHotUpdaterCoreMetadata,
|
|
3
3
|
type RuntimeHotUpdaterAPI,
|
|
4
4
|
} from "../createHotUpdaterCore";
|
|
5
|
+
import type { AnyHotUpdaterPlugin } from "../plugins/definePlugin";
|
|
5
6
|
import { type Migrator, type SchemaGenerator } from "./types";
|
|
6
7
|
|
|
7
8
|
export { createDatabaseCoreApi, type CoreApi } from "../core/api";
|
|
@@ -11,8 +12,19 @@ export {
|
|
|
11
12
|
type MeasuredDatabase,
|
|
12
13
|
type MeasuredDatabaseOptions,
|
|
13
14
|
} from "../assembly/databasePlugins";
|
|
15
|
+
export {
|
|
16
|
+
clientAuthOf,
|
|
17
|
+
clientPluginsOf,
|
|
18
|
+
generateClientCredential,
|
|
19
|
+
pluginCommandsOf,
|
|
20
|
+
provisionClientCredential,
|
|
21
|
+
type ClientAuthSpec,
|
|
22
|
+
type ClientCredentialSpec,
|
|
23
|
+
type ClientPluginSpec,
|
|
24
|
+
type PluginCommandEntry,
|
|
25
|
+
type ProvisionedClientCredential,
|
|
26
|
+
} from "../assembly/pluginCli";
|
|
14
27
|
export { targetBaseCandidateKey } from "../core/baseCandidates";
|
|
15
|
-
export * from "./createBundleDiff";
|
|
16
28
|
export {
|
|
17
29
|
generateEngineSql,
|
|
18
30
|
settingsStatements,
|
|
@@ -44,11 +56,16 @@ const getDBMetadata = (hotUpdater: HotUpdaterDBTarget) => {
|
|
|
44
56
|
return metadata;
|
|
45
57
|
};
|
|
46
58
|
|
|
59
|
+
/** A server's plugins as configured, whose commands the CLI adds. */
|
|
60
|
+
export const serverPluginsOf = (
|
|
61
|
+
hotUpdater: HotUpdaterDBTarget,
|
|
62
|
+
): readonly AnyHotUpdaterPlugin[] => getDBMetadata(hotUpdater).plugins;
|
|
63
|
+
|
|
47
64
|
/** Whether `hot-updater db generate` writes schema files for a server's database. */
|
|
48
65
|
export const generatesSchema = (hotUpdater: HotUpdaterDBTarget): boolean =>
|
|
49
66
|
getDBMetadata(hotUpdater).database.generateSchema !== undefined;
|
|
50
67
|
|
|
51
|
-
/** The migrator for a server's database:
|
|
68
|
+
/** The migrator for a server's database: core's tables and its plugins' tables. */
|
|
52
69
|
export function createMigrator(hotUpdater: HotUpdaterDBTarget): Migrator {
|
|
53
70
|
const { database, target } = getDBMetadata(hotUpdater);
|
|
54
71
|
if (database.createMigrator === undefined) {
|
|
@@ -10,13 +10,18 @@ import { prismaAdapter } from "../adapters/prisma";
|
|
|
10
10
|
import { createHotUpdater } from "../createHotUpdaterCore";
|
|
11
11
|
import { HotUpdaterSchemaMigrationRequiredError } from "../database/fence";
|
|
12
12
|
import { defineTable } from "../database/schema";
|
|
13
|
-
import {
|
|
13
|
+
import { createTableStatements } from "../database/sql/sqlAdapter";
|
|
14
|
+
import { apiKeys } from "../plugins/api-keys";
|
|
15
|
+
import {
|
|
16
|
+
type AnyHotUpdaterPlugin,
|
|
17
|
+
definePlugin,
|
|
18
|
+
} from "../plugins/definePlugin";
|
|
14
19
|
import { insights } from "../plugins/insights";
|
|
15
20
|
import {
|
|
16
|
-
|
|
21
|
+
coreTarget,
|
|
17
22
|
createEngineDatabase,
|
|
18
23
|
toolingTargetOf,
|
|
19
|
-
} from "./
|
|
24
|
+
} from "./coreDatabase";
|
|
20
25
|
import { createMigrator, generateSchema } from "./index";
|
|
21
26
|
|
|
22
27
|
const notes = definePlugin({
|
|
@@ -56,21 +61,33 @@ const migrate = async (hotUpdater: { readonly adapterName: string }) => {
|
|
|
56
61
|
const memoryDatabase = () =>
|
|
57
62
|
createEngineDatabase({ name: "memory", adapter: createMemoryAdapter() });
|
|
58
63
|
|
|
59
|
-
describe("
|
|
60
|
-
it("adds each
|
|
61
|
-
expect(toolingTargetOf([
|
|
64
|
+
describe("plugin tables in db tooling", () => {
|
|
65
|
+
it("adds each plugin's tables and settings row to core's, prefixed by its id unless it keeps its names", () => {
|
|
66
|
+
expect(toolingTargetOf([])).toBe(coreTarget);
|
|
62
67
|
const target = toolingTargetOf([insights(), notes]);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const names = target.schema.tables.map(({ name }) => name);
|
|
69
|
+
expect(names.slice(0, coreTarget.schema.tables.length)).toEqual(
|
|
70
|
+
coreTarget.schema.tables.map(({ name }) => name),
|
|
71
|
+
);
|
|
72
|
+
expect(names).toContain("bundle_events");
|
|
73
|
+
expect(names.at(-1)).toBe("notes_notes");
|
|
67
74
|
expect(target.settings).toEqual({
|
|
68
|
-
...
|
|
75
|
+
...coreTarget.settings,
|
|
76
|
+
"schema.insights": insights().schemaVersion,
|
|
69
77
|
"schema.notes": "2",
|
|
70
78
|
});
|
|
71
79
|
});
|
|
72
80
|
|
|
73
|
-
it("
|
|
81
|
+
it("fits Hot Updater's own plugins' tables within MySQL's key limit", () => {
|
|
82
|
+
expect(() =>
|
|
83
|
+
createTableStatements(
|
|
84
|
+
"mysql",
|
|
85
|
+
toolingTargetOf([insights(), apiKeys()]).schema.tables,
|
|
86
|
+
),
|
|
87
|
+
).not.toThrow();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("migrates an engine database's core and plugin tables with its adapter", async () => {
|
|
74
91
|
const hotUpdater = createHotUpdater({
|
|
75
92
|
database: memoryDatabase(),
|
|
76
93
|
plugins: [notes],
|
|
@@ -180,7 +197,7 @@ describe("third-party plugin tables in db tooling", () => {
|
|
|
180
197
|
).toContain("model notes_notes {");
|
|
181
198
|
});
|
|
182
199
|
|
|
183
|
-
it("
|
|
200
|
+
it("checks a plugin's id and tables against core and the other plugins the server runs", () => {
|
|
184
201
|
const named = definePlugin({
|
|
185
202
|
id: "insights",
|
|
186
203
|
schemaVersion: "1",
|
|
@@ -197,22 +214,35 @@ describe("third-party plugin tables in db tooling", () => {
|
|
|
197
214
|
},
|
|
198
215
|
init: () => ({ api: {} }),
|
|
199
216
|
});
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
217
|
+
const unprefixed = definePlugin({
|
|
218
|
+
id: "shadowCore",
|
|
219
|
+
namespace: false,
|
|
220
|
+
schemaVersion: "1",
|
|
221
|
+
schema: {
|
|
222
|
+
channels: defineTable({ id: { type: "string" } }, { key: ["id"] }),
|
|
223
|
+
},
|
|
224
|
+
init: () => ({ api: {} }),
|
|
225
|
+
});
|
|
226
|
+
const start = (plugins: readonly AnyHotUpdaterPlugin[]) => () =>
|
|
209
227
|
createHotUpdater({
|
|
210
228
|
database: memoryDatabase(),
|
|
211
|
-
plugins
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
229
|
+
plugins,
|
|
230
|
+
...(plugins.some(({ provides }) => provides?.clientAuth)
|
|
231
|
+
? {}
|
|
232
|
+
: { clientAccess: "public" }),
|
|
233
|
+
} as Parameters<typeof createHotUpdater>[0]);
|
|
234
|
+
|
|
235
|
+
// Only the plugins a server runs get tables, so these fit on their own.
|
|
236
|
+
expect(start([named])).not.toThrow();
|
|
237
|
+
expect(start([tabled])).not.toThrow();
|
|
238
|
+
expect(start([named, insights()])).toThrow(
|
|
239
|
+
'Plugin "insights" is registered twice.',
|
|
240
|
+
);
|
|
241
|
+
expect(start([tabled, apiKeys()])).toThrow(
|
|
242
|
+
'apiKeys.api_keys: table "api_keys" is also declared by api.keys',
|
|
243
|
+
);
|
|
244
|
+
expect(start([unprefixed])).toThrow(
|
|
245
|
+
'shadowCore.channels: table "channels" is also declared by core.channels',
|
|
216
246
|
);
|
|
217
247
|
});
|
|
218
248
|
});
|