@termix-ssh/plugin-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/cli/commands/build.mjs +246 -0
- package/cli/commands/migrations.mjs +248 -0
- package/cli/commands/pack.mjs +42 -0
- package/cli/commands/sign.mjs +83 -0
- package/cli/commands/test.mjs +33 -0
- package/cli/commands/validate.mjs +215 -0
- package/cli/index.mjs +65 -0
- package/cli/lib/changelog.mjs +126 -0
- package/cli/lib/externals.mjs +46 -0
- package/cli/lib/plugin-dir.mjs +29 -0
- package/cli/lib/signing.mjs +86 -0
- package/cli/lib/static-url-imports.mjs +54 -0
- package/cli/lib/tmxplug.mjs +106 -0
- package/cli/patches/guacamole-common-js-flush.cjs +76 -0
- package/cli/patches/xterm-android-ime.cjs +130 -0
- package/client.d.ts +13 -0
- package/dist/backend.d.ts +1777 -0
- package/dist/backend.js +84 -0
- package/dist/capabilities.d.ts +38 -0
- package/dist/capabilities.js +71 -0
- package/dist/db.d.ts +112 -0
- package/dist/db.js +189 -0
- package/dist/ddl.d.ts +76 -0
- package/dist/ddl.js +678 -0
- package/dist/desktop.d.ts +346 -0
- package/dist/desktop.js +5 -0
- package/dist/frontend.d.ts +1206 -0
- package/dist/frontend.js +388 -0
- package/dist/host/chunk-F3SDZDDG.js +6017 -0
- package/dist/host/chunk-XGWE7BTJ.js +90 -0
- package/dist/host/chunk-XH5NXL6Y.js +56314 -0
- package/dist/host/chunk-YC2MAKSP.js +14 -0
- package/dist/host/linked-server-FZGWQW32.js +12 -0
- package/dist/host/rbac-api-6KDMBVDZ.js +52 -0
- package/dist/host/testing-host.js +414 -0
- package/dist/host/theme.css +91 -0
- package/dist/host/types/src/types/auth-protocols.d.ts +10 -0
- package/dist/host/types/src/types/connection-log.d.ts +13 -0
- package/dist/host/types/src/types/credential-sidebar-preferences.d.ts +38 -0
- package/dist/host/types/src/types/host-defaults.d.ts +60 -0
- package/dist/host/types/src/types/host-sidebar-preferences.d.ts +55 -0
- package/dist/host/types/src/types/index.d.ts +298 -0
- package/dist/host/types/src/types/keybindings.d.ts +27 -0
- package/dist/host/types/src/types/ui-preferences.d.ts +102 -0
- package/dist/host/types/src/types/ui-types.d.ts +260 -0
- package/dist/host/types/src/ui/api/admin-user-data-api.d.ts +11 -0
- package/dist/host/types/src/ui/api/auth-methods-api.d.ts +63 -0
- package/dist/host/types/src/ui/api/credential-sidebar-preferences-api.d.ts +3 -0
- package/dist/host/types/src/ui/api/credentials-api.d.ts +56 -0
- package/dist/host/types/src/ui/api/dashboard-api.d.ts +23 -0
- package/dist/host/types/src/ui/api/external-account-api.d.ts +8 -0
- package/dist/host/types/src/ui/api/host-sidebar-preferences-api.d.ts +3 -0
- package/dist/host/types/src/ui/api/host-status-api.d.ts +11 -0
- package/dist/host/types/src/ui/api/open-tabs-api.d.ts +74 -0
- package/dist/host/types/src/ui/api/plugins-api.d.ts +128 -0
- package/dist/host/types/src/ui/api/rbac-api.d.ts +167 -0
- package/dist/host/types/src/ui/api/settings-api.d.ts +22 -0
- package/dist/host/types/src/ui/api/ssh-host-management-api.d.ts +65 -0
- package/dist/host/types/src/ui/api/sync-api.d.ts +93 -0
- package/dist/host/types/src/ui/api/system-status-api.d.ts +49 -0
- package/dist/host/types/src/ui/api/ui-preferences-api.d.ts +8 -0
- package/dist/host/types/src/ui/api/user-management-api.d.ts +80 -0
- package/dist/host/types/src/ui/auth/Auth.d.ts +12 -0
- package/dist/host/types/src/ui/auth/auth-bits.d.ts +11 -0
- package/dist/host/types/src/ui/auth/silent-signin.d.ts +2 -0
- package/dist/host/types/src/ui/components/FullScreenAppWrapper.d.ts +9 -0
- package/dist/host/types/src/ui/components/alert-dialog.d.ts +14 -0
- package/dist/host/types/src/ui/components/alert.d.ts +9 -0
- package/dist/host/types/src/ui/components/badge.d.ts +9 -0
- package/dist/host/types/src/ui/components/button.d.ts +13 -0
- package/dist/host/types/src/ui/components/card-grid/CardGridCanvas.d.ts +22 -0
- package/dist/host/types/src/ui/components/card-grid/layout-utils.d.ts +26 -0
- package/dist/host/types/src/ui/components/card-grid/types.d.ts +29 -0
- package/dist/host/types/src/ui/components/card-masonry.d.ts +16 -0
- package/dist/host/types/src/ui/components/card.d.ts +11 -0
- package/dist/host/types/src/ui/components/charts/BarSeries.d.ts +16 -0
- package/dist/host/types/src/ui/components/charts/LineChart.d.ts +17 -0
- package/dist/host/types/src/ui/components/charts/RadialGauge.d.ts +18 -0
- package/dist/host/types/src/ui/components/charts/Sparkline.d.ts +22 -0
- package/dist/host/types/src/ui/components/charts/StatRow.d.ts +15 -0
- package/dist/host/types/src/ui/components/charts/geometry.d.ts +20 -0
- package/dist/host/types/src/ui/components/charts/index.d.ts +4 -0
- package/dist/host/types/src/ui/components/checkbox.d.ts +4 -0
- package/dist/host/types/src/ui/components/connection/ConnectionGate.d.ts +56 -0
- package/dist/host/types/src/ui/components/connection/ConnectionLogPanel.d.ts +9 -0
- package/dist/host/types/src/ui/components/connection/ConnectionScreen.d.ts +34 -0
- package/dist/host/types/src/ui/components/connection/connection-status.d.ts +1 -0
- package/dist/host/types/src/ui/components/data-view.d.ts +32 -0
- package/dist/host/types/src/ui/components/dialog.d.ts +17 -0
- package/dist/host/types/src/ui/components/dropdown-menu.d.ts +29 -0
- package/dist/host/types/src/ui/components/empty-state.d.ts +19 -0
- package/dist/host/types/src/ui/components/form-fields.d.ts +115 -0
- package/dist/host/types/src/ui/components/input.d.ts +3 -0
- package/dist/host/types/src/ui/components/kbd.d.ts +12 -0
- package/dist/host/types/src/ui/components/label.d.ts +4 -0
- package/dist/host/types/src/ui/components/manager-card.d.ts +25 -0
- package/dist/host/types/src/ui/components/meter.d.ts +14 -0
- package/dist/host/types/src/ui/components/metric-card.d.ts +18 -0
- package/dist/host/types/src/ui/components/panel-layout.d.ts +83 -0
- package/dist/host/types/src/ui/components/password-input.d.ts +4 -0
- package/dist/host/types/src/ui/components/popover.d.ts +7 -0
- package/dist/host/types/src/ui/components/scroll-area.d.ts +5 -0
- package/dist/host/types/src/ui/components/section-card.d.ts +29 -0
- package/dist/host/types/src/ui/components/select.d.ts +15 -0
- package/dist/host/types/src/ui/components/select2.d.ts +8 -0
- package/dist/host/types/src/ui/components/separator.d.ts +4 -0
- package/dist/host/types/src/ui/components/sheet.d.ts +14 -0
- package/dist/host/types/src/ui/components/sidebar.d.ts +69 -0
- package/dist/host/types/src/ui/components/skeleton.d.ts +2 -0
- package/dist/host/types/src/ui/components/slider.d.ts +4 -0
- package/dist/host/types/src/ui/components/sonner.d.ts +4 -0
- package/dist/host/types/src/ui/components/surface/prompt-styles.d.ts +4 -0
- package/dist/host/types/src/ui/components/surface/surface-scope.d.ts +79 -0
- package/dist/host/types/src/ui/components/switch.d.ts +4 -0
- package/dist/host/types/src/ui/components/tab-strip.d.ts +19 -0
- package/dist/host/types/src/ui/components/tabs.d.ts +7 -0
- package/dist/host/types/src/ui/components/textarea.d.ts +4 -0
- package/dist/host/types/src/ui/components/theme-provider.d.ts +14 -0
- package/dist/host/types/src/ui/components/tooltip.d.ts +7 -0
- package/dist/host/types/src/ui/contexts/BrandingContext.d.ts +15 -0
- package/dist/host/types/src/ui/contexts/UiPreferencesContext.d.ts +38 -0
- package/dist/host/types/src/ui/contexts/branding-document.d.ts +10 -0
- package/dist/host/types/src/ui/dashboard/dashboard-cards-registry.d.ts +30 -0
- package/dist/host/types/src/ui/hooks/use-action-slot.d.ts +10 -0
- package/dist/host/types/src/ui/hooks/use-adaptive-polling.d.ts +5 -0
- package/dist/host/types/src/ui/hooks/use-mobile.d.ts +1 -0
- package/dist/host/types/src/ui/hooks/use-permissions.d.ts +29 -0
- package/dist/host/types/src/ui/hooks/use-status-color-scheme.d.ts +12 -0
- package/dist/host/types/src/ui/hooks/use-sync-status.d.ts +4 -0
- package/dist/host/types/src/ui/hooks/useSshAuthProviders.d.ts +17 -0
- package/dist/host/types/src/ui/i18n/i18n.d.ts +52 -0
- package/dist/host/types/src/ui/lib/ServerStatusContext.d.ts +33 -0
- package/dist/host/types/src/ui/lib/adaptive-polling.d.ts +29 -0
- package/dist/host/types/src/ui/lib/adaptive-resource-budget.d.ts +39 -0
- package/dist/host/types/src/ui/lib/app-keyboard-shortcuts.d.ts +8 -0
- package/dist/host/types/src/ui/lib/base-path.d.ts +1 -0
- package/dist/host/types/src/ui/lib/clipboard-provider.d.ts +9 -0
- package/dist/host/types/src/ui/lib/clipboard.d.ts +20 -0
- package/dist/host/types/src/ui/lib/connection-origin.d.ts +47 -0
- package/dist/host/types/src/ui/lib/db-health-monitor.d.ts +34 -0
- package/dist/host/types/src/ui/lib/device-id.d.ts +1 -0
- package/dist/host/types/src/ui/lib/electron.d.ts +1 -0
- package/dist/host/types/src/ui/lib/embedded-server-status.d.ts +15 -0
- package/dist/host/types/src/ui/lib/error-message.d.ts +9 -0
- package/dist/host/types/src/ui/lib/frontend-logger.d.ts +67 -0
- package/dist/host/types/src/ui/lib/global-shortcut-handler.d.ts +3 -0
- package/dist/host/types/src/ui/lib/host-defaults-context.d.ts +48 -0
- package/dist/host/types/src/ui/lib/hosts-request-cache.d.ts +9 -0
- package/dist/host/types/src/ui/lib/keybinding-match.d.ts +3 -0
- package/dist/host/types/src/ui/lib/linked-server.d.ts +10 -0
- package/dist/host/types/src/ui/lib/local-adaptive-engine.d.ts +40 -0
- package/dist/host/types/src/ui/lib/permissions.d.ts +16 -0
- package/dist/host/types/src/ui/lib/plugin-i18n.d.ts +11 -0
- package/dist/host/types/src/ui/lib/plugin-icon.d.ts +11 -0
- package/dist/host/types/src/ui/lib/plugin-transport.d.ts +50 -0
- package/dist/host/types/src/ui/lib/registry.d.ts +29 -0
- package/dist/host/types/src/ui/lib/rem.d.ts +7 -0
- package/dist/host/types/src/ui/lib/remote-server-api.d.ts +11 -0
- package/dist/host/types/src/ui/lib/server-status-store.d.ts +37 -0
- package/dist/host/types/src/ui/lib/sync-events.d.ts +2 -0
- package/dist/host/types/src/ui/lib/sync-status.d.ts +6 -0
- package/dist/host/types/src/ui/lib/tab-jump-hotkey.d.ts +4 -0
- package/dist/host/types/src/ui/lib/theme.d.ts +21 -0
- package/dist/host/types/src/ui/lib/ttl-request-cache.d.ts +10 -0
- package/dist/host/types/src/ui/lib/usage-color.d.ts +5 -0
- package/dist/host/types/src/ui/lib/utils.d.ts +2 -0
- package/dist/host/types/src/ui/lib/visible-interval.d.ts +5 -0
- package/dist/host/types/src/ui/lib/widget-title.d.ts +8 -0
- package/dist/host/types/src/ui/lib/ws-auth.d.ts +1 -0
- package/dist/host/types/src/ui/main-axios.d.ts +170 -0
- package/dist/host/types/src/ui/plugin-host/PluginViewPlaceholder.d.ts +13 -0
- package/dist/host/types/src/ui/plugin-host/app.d.ts +18 -0
- package/dist/host/types/src/ui/plugin-host/auth-registry.d.ts +50 -0
- package/dist/host/types/src/ui/plugin-host/bridge.d.ts +26 -0
- package/dist/host/types/src/ui/plugin-host/component-registry.d.ts +24 -0
- package/dist/host/types/src/ui/plugin-host/desktop.d.ts +7 -0
- package/dist/host/types/src/ui/plugin-host/extension-registry.d.ts +16 -0
- package/dist/host/types/src/ui/plugin-host/host-scope.d.ts +26 -0
- package/dist/host/types/src/ui/plugin-host/loader.d.ts +52 -0
- package/dist/host/types/src/ui/plugin-host/plugin-store.d.ts +40 -0
- package/dist/host/types/src/ui/plugin-host/scope.d.ts +20 -0
- package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +99 -0
- package/dist/host/types/src/ui/plugin-host/shell-bridge.d.ts +67 -0
- package/dist/host/types/src/ui/plugin-host/view-ownership.d.ts +31 -0
- package/dist/host/types/src/ui/settings/HostPluginSections.d.ts +31 -0
- package/dist/host/types/src/ui/settings/SettingsFields.d.ts +22 -0
- package/dist/host/types/src/ui/settings/host-feature-tabs.d.ts +11 -0
- package/dist/host/types/src/ui/settings/settings-components.d.ts +32 -0
- package/dist/host/types/src/ui/settings/settings-fields-util.d.ts +11 -0
- package/dist/host/types/src/ui/shell/ActionSlot.d.ts +41 -0
- package/dist/host/types/src/ui/shell/TabContext.d.ts +30 -0
- package/dist/host/types/src/ui/shell/action-registry.d.ts +99 -0
- package/dist/host/types/src/ui/shell/keybinding-registry.d.ts +62 -0
- package/dist/host/types/src/ui/shell/palette-registry.d.ts +55 -0
- package/dist/host/types/src/ui/shell/panel-registry.d.ts +24 -0
- package/dist/host/types/src/ui/shell/tab-registry.d.ts +92 -0
- package/dist/host/types/src/ui/sidebar/HostManagerTabs.d.ts +65 -0
- package/dist/host/types/src/ui/sidebar/host-contributions.d.ts +72 -0
- package/dist/host/types/src/ui/sidebar/host-defaults/catalog.d.ts +17 -0
- package/dist/host/types/src/ui/sidebar/host-protocols.d.ts +49 -0
- package/dist/host/types/src/ui/sidebar/quick-connect-host.d.ts +21 -0
- package/dist/host/types/src/ui/sidebar/rail-items.d.ts +103 -0
- package/dist/host/types/src/ui/ssh/connection-log/ConnectionLogContext.d.ts +17 -0
- package/dist/host/types/src/ui/ssh/dialogs/BrowserSignInDialog.d.ts +18 -0
- package/dist/host/types/src/ui/ssh/dialogs/HostKeyVerificationDialog.d.ts +17 -0
- package/dist/host/types/src/ui/ssh/dialogs/PassphraseDialog.d.ts +15 -0
- package/dist/host/types/src/ui/ssh/dialogs/SSHAuthDialog.d.ts +20 -0
- package/dist/host/types/src/ui/ssh/dialogs/TOTPDialog.d.ts +13 -0
- package/dist/host/ui.d.ts +1 -0
- package/dist/host/ui.js +31754 -0
- package/dist/host/use-permissions-NULZL43J.js +18 -0
- package/dist/host-commands.d.ts +89 -0
- package/dist/host-commands.js +387 -0
- package/dist/manifest.d.ts +461 -0
- package/dist/manifest.js +1499 -0
- package/dist/settings.d.ts +43 -0
- package/dist/settings.js +168 -0
- package/dist/ssh-certs.d.ts +25 -0
- package/dist/ssh-certs.js +234 -0
- package/dist/ssh-options.d.ts +23 -0
- package/dist/ssh-options.js +1 -0
- package/dist/table-builder.d.ts +26 -0
- package/dist/table-builder.js +87 -0
- package/dist/testing/safe-ports.d.ts +10 -0
- package/dist/testing/safe-ports.js +59 -0
- package/dist/testing/setup.d.ts +7 -0
- package/dist/testing/setup.js +146 -0
- package/dist/testing.d.ts +537 -0
- package/dist/testing.js +1451 -0
- package/dist/vitest-preset.d.ts +54 -0
- package/dist/vitest-preset.js +96 -0
- package/package.json +143 -0
- package/schema/changelog.schema.json +67 -0
- package/schema/manifest.schema.json +1160 -0
- package/tsconfig.plugin.json +31 -0
package/dist/ddl.js
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DDL a table definition becomes, per dialect.
|
|
3
|
+
*
|
|
4
|
+
* Lives in the SDK because two things need the identical mapping: the CLI,
|
|
5
|
+
* which writes a plugin's migration files, and the server, which applies them.
|
|
6
|
+
* A second copy would drift and produce a migration the runner could not
|
|
7
|
+
* reproduce.
|
|
8
|
+
*
|
|
9
|
+
* The mapping itself is the one scripts/generate-dialect-schema.cjs applies to
|
|
10
|
+
* core's schema, kept deliberately in step with it.
|
|
11
|
+
*/
|
|
12
|
+
import { KEY_LENGTH, prefixedTableName, tablePrefix } from "./db.js";
|
|
13
|
+
/** The core tables a plugin may point a foreign key at, and nothing else. */
|
|
14
|
+
export const REFERENCEABLE = {
|
|
15
|
+
refUser: { table: "users", column: "id" },
|
|
16
|
+
refHost: { table: "ssh_data", column: "id" },
|
|
17
|
+
};
|
|
18
|
+
function camelToSnake(value) {
|
|
19
|
+
return value.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
20
|
+
}
|
|
21
|
+
/** The SQL column name for a declared property. */
|
|
22
|
+
export function columnName(property, column) {
|
|
23
|
+
void column;
|
|
24
|
+
return camelToSnake(property);
|
|
25
|
+
}
|
|
26
|
+
function quote(dialect, identifier) {
|
|
27
|
+
if (dialect === "mysql")
|
|
28
|
+
return "`" + identifier + "`";
|
|
29
|
+
return '"' + identifier + '"';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The physical type for a column on one dialect.
|
|
33
|
+
*
|
|
34
|
+
* Mirrors generate-dialect-schema.cjs: an autoincrement key is serial /
|
|
35
|
+
* auto_increment, a boolean is native off sqlite, and anything carrying a key
|
|
36
|
+
* has to be varchar because MySQL will not index TEXT.
|
|
37
|
+
*/
|
|
38
|
+
function columnType(dialect, column, keyed) {
|
|
39
|
+
switch (column.type) {
|
|
40
|
+
case "id":
|
|
41
|
+
if (dialect === "postgres")
|
|
42
|
+
return "serial";
|
|
43
|
+
if (dialect === "mysql")
|
|
44
|
+
return "int AUTO_INCREMENT";
|
|
45
|
+
return "integer";
|
|
46
|
+
case "integer":
|
|
47
|
+
return dialect === "mysql" ? "int" : "integer";
|
|
48
|
+
case "bigint":
|
|
49
|
+
return dialect === "sqlite" ? "integer" : "bigint";
|
|
50
|
+
case "real":
|
|
51
|
+
if (dialect === "sqlite")
|
|
52
|
+
return "real";
|
|
53
|
+
return dialect === "mysql" ? "double" : "double precision";
|
|
54
|
+
case "boolean":
|
|
55
|
+
return dialect === "sqlite" ? "integer" : "boolean";
|
|
56
|
+
case "varchar":
|
|
57
|
+
return dialect === "sqlite"
|
|
58
|
+
? "text"
|
|
59
|
+
: `varchar(${column.length ?? KEY_LENGTH})`;
|
|
60
|
+
case "refUser":
|
|
61
|
+
return dialect === "sqlite" ? "text" : `varchar(${KEY_LENGTH})`;
|
|
62
|
+
case "refHost":
|
|
63
|
+
return dialect === "mysql" ? "int" : "integer";
|
|
64
|
+
default:
|
|
65
|
+
// A keyed string has to be indexable, so it narrows to varchar even
|
|
66
|
+
// though it was declared unbounded. defineTable refuses to index a
|
|
67
|
+
// text/json column, so in practice this only catches a primary key.
|
|
68
|
+
if (keyed && dialect !== "sqlite")
|
|
69
|
+
return `varchar(${KEY_LENGTH})`;
|
|
70
|
+
return "text";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Column types that are TEXT on MySQL when nothing keys them. */
|
|
74
|
+
const TEXT_ON_MYSQL = new Set([
|
|
75
|
+
"text",
|
|
76
|
+
"json",
|
|
77
|
+
"encryptedText",
|
|
78
|
+
"timestamp",
|
|
79
|
+
]);
|
|
80
|
+
function defaultClause(dialect, column) {
|
|
81
|
+
if (column.defaultNow) {
|
|
82
|
+
// MySQL rejects a bare DEFAULT CURRENT_TIMESTAMP on a text column; an
|
|
83
|
+
// expression default is written parenthesised. MariaDB takes either.
|
|
84
|
+
return dialect === "mysql"
|
|
85
|
+
? " DEFAULT (CURRENT_TIMESTAMP)"
|
|
86
|
+
: " DEFAULT CURRENT_TIMESTAMP";
|
|
87
|
+
}
|
|
88
|
+
if (column.defaultValue === undefined)
|
|
89
|
+
return "";
|
|
90
|
+
const value = column.defaultValue;
|
|
91
|
+
if (value === null)
|
|
92
|
+
return " DEFAULT NULL";
|
|
93
|
+
if (typeof value === "boolean") {
|
|
94
|
+
return dialect === "sqlite"
|
|
95
|
+
? ` DEFAULT ${value ? 1 : 0}`
|
|
96
|
+
: ` DEFAULT ${value ? "true" : "false"}`;
|
|
97
|
+
}
|
|
98
|
+
if (typeof value === "number")
|
|
99
|
+
return ` DEFAULT ${value}`;
|
|
100
|
+
const literal = `'${String(value).replace(/'/g, "''")}'`;
|
|
101
|
+
// MySQL refuses a literal default on a TEXT column but takes an expression.
|
|
102
|
+
if (dialect === "mysql" && TEXT_ON_MYSQL.has(column.type)) {
|
|
103
|
+
return ` DEFAULT (${literal})`;
|
|
104
|
+
}
|
|
105
|
+
return ` DEFAULT ${literal}`;
|
|
106
|
+
}
|
|
107
|
+
/** Every column that carries a key, and so must be indexable on MySQL. */
|
|
108
|
+
export function keyedColumns(definition) {
|
|
109
|
+
const keyed = new Set();
|
|
110
|
+
for (const [property, column] of Object.entries(definition.columns)) {
|
|
111
|
+
if (column.primaryKey || column.unique)
|
|
112
|
+
keyed.add(property);
|
|
113
|
+
if (column.type === "refUser" || column.type === "refHost") {
|
|
114
|
+
keyed.add(property);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const index of definition.indexes) {
|
|
118
|
+
for (const property of index.columns)
|
|
119
|
+
keyed.add(property);
|
|
120
|
+
}
|
|
121
|
+
return keyed;
|
|
122
|
+
}
|
|
123
|
+
function indexStatement(dialect, physical, definition, entry) {
|
|
124
|
+
const columns = entry.columns
|
|
125
|
+
.map((property) => quote(dialect, columnName(property, definition.columns[property])))
|
|
126
|
+
.join(", ");
|
|
127
|
+
const unique = entry.unique ? "UNIQUE " : "";
|
|
128
|
+
// MySQL has no CREATE INDEX IF NOT EXISTS. A plugin migration runs once,
|
|
129
|
+
// recorded in plugin_migrations, so the guard is the ledger, not the DDL.
|
|
130
|
+
const guard = dialect === "mysql" ? "" : "IF NOT EXISTS ";
|
|
131
|
+
return `CREATE ${unique}INDEX ${guard}${quote(dialect, entry.name)} ON ${quote(dialect, physical)} (${columns});`;
|
|
132
|
+
}
|
|
133
|
+
function createStatements(dialect, physical, definition) {
|
|
134
|
+
const keyed = keyedColumns(definition);
|
|
135
|
+
const lines = [];
|
|
136
|
+
const constraints = [];
|
|
137
|
+
for (const [property, column] of Object.entries(definition.columns)) {
|
|
138
|
+
const name = columnName(property, column);
|
|
139
|
+
const type = columnType(dialect, column, keyed.has(property));
|
|
140
|
+
let line = ` ${quote(dialect, name)} ${type}`;
|
|
141
|
+
if (column.type === "id") {
|
|
142
|
+
line += " PRIMARY KEY";
|
|
143
|
+
if (dialect === "sqlite")
|
|
144
|
+
line += " AUTOINCREMENT";
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
if (column.primaryKey)
|
|
148
|
+
line += " PRIMARY KEY";
|
|
149
|
+
if (column.notNull)
|
|
150
|
+
line += " NOT NULL";
|
|
151
|
+
if (column.unique)
|
|
152
|
+
line += " UNIQUE";
|
|
153
|
+
line += defaultClause(dialect, column);
|
|
154
|
+
}
|
|
155
|
+
lines.push(line);
|
|
156
|
+
const reference = column.type === "refUser"
|
|
157
|
+
? REFERENCEABLE.refUser
|
|
158
|
+
: column.type === "refHost"
|
|
159
|
+
? REFERENCEABLE.refHost
|
|
160
|
+
: null;
|
|
161
|
+
if (reference) {
|
|
162
|
+
constraints.push(` FOREIGN KEY (${quote(dialect, name)}) REFERENCES ${quote(dialect, reference.table)} (${quote(dialect, reference.column)}) ON DELETE CASCADE`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const body = [...lines, ...constraints].join(",\n");
|
|
166
|
+
return {
|
|
167
|
+
table: `CREATE TABLE IF NOT EXISTS ${quote(dialect, physical)} (\n${body}\n);`,
|
|
168
|
+
indexes: definition.indexes.map((entry) => indexStatement(dialect, physical, definition, entry)),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/** The CREATE TABLE and CREATE INDEX statements for one definition. */
|
|
172
|
+
export function createTableSql(dialect, pluginId, definition) {
|
|
173
|
+
const physical = prefixedTableName(pluginId, definition.name);
|
|
174
|
+
const { table, indexes } = createStatements(dialect, physical, definition);
|
|
175
|
+
return [table, ...indexes];
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Takes over a legacy core table by renaming it into the plugin's namespace.
|
|
179
|
+
*
|
|
180
|
+
* A plain SQL migration cannot ask whether the legacy table exists, so it
|
|
181
|
+
* creates it first when it does not (a fresh install) and then renames either
|
|
182
|
+
* way. Both paths end with the same table and the same rows.
|
|
183
|
+
*
|
|
184
|
+
* Indexes are created on the legacy name with IF NOT EXISTS before the rename,
|
|
185
|
+
* which is why an adopted definition keeps the legacy index names: an index
|
|
186
|
+
* that already exists is left alone and follows the table. MySQL has no
|
|
187
|
+
* CREATE INDEX IF NOT EXISTS, so there none are emitted. That is safe because
|
|
188
|
+
* every legacy table reached MySQL through core's drizzle migrations, which
|
|
189
|
+
* already created its indexes.
|
|
190
|
+
*/
|
|
191
|
+
export function adoptTableSql(dialect, pluginId, definition) {
|
|
192
|
+
const legacy = definition.adopts;
|
|
193
|
+
if (!legacy) {
|
|
194
|
+
throw new Error(`Table "${definition.name}" does not adopt a legacy table`);
|
|
195
|
+
}
|
|
196
|
+
const physical = prefixedTableName(pluginId, definition.name);
|
|
197
|
+
const { table, indexes } = createStatements(dialect, legacy, definition);
|
|
198
|
+
const rename = dialect === "mysql"
|
|
199
|
+
? `RENAME TABLE ${quote(dialect, legacy)} TO ${quote(dialect, physical)};`
|
|
200
|
+
: `ALTER TABLE ${quote(dialect, legacy)} RENAME TO ${quote(dialect, physical)};`;
|
|
201
|
+
return [table, ...(dialect === "mysql" ? [] : indexes), rename];
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Splits a migration file into statements.
|
|
205
|
+
*
|
|
206
|
+
* Deliberately simple: statements end at a semicolon that is not inside a
|
|
207
|
+
* string literal or a comment. A plugin needing more than that should put the
|
|
208
|
+
* logic in its own code, not in DDL.
|
|
209
|
+
*/
|
|
210
|
+
export function splitStatements(source) {
|
|
211
|
+
const statements = [];
|
|
212
|
+
let current = "";
|
|
213
|
+
let quoteChar = null;
|
|
214
|
+
let lineComment = false;
|
|
215
|
+
let blockComment = false;
|
|
216
|
+
for (let i = 0; i < source.length; i++) {
|
|
217
|
+
const char = source[i];
|
|
218
|
+
const next = source[i + 1];
|
|
219
|
+
if (lineComment) {
|
|
220
|
+
current += char;
|
|
221
|
+
if (char === "\n")
|
|
222
|
+
lineComment = false;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (blockComment) {
|
|
226
|
+
current += char;
|
|
227
|
+
if (char === "*" && next === "/") {
|
|
228
|
+
current += next;
|
|
229
|
+
i++;
|
|
230
|
+
blockComment = false;
|
|
231
|
+
}
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (quoteChar) {
|
|
235
|
+
current += char;
|
|
236
|
+
// '' inside a quoted string is an escaped quote, not a terminator.
|
|
237
|
+
if (char === quoteChar) {
|
|
238
|
+
if (next === quoteChar) {
|
|
239
|
+
current += next;
|
|
240
|
+
i++;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
quoteChar = null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (char === "-" && next === "-") {
|
|
249
|
+
lineComment = true;
|
|
250
|
+
current += char;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (char === "/" && next === "*") {
|
|
254
|
+
blockComment = true;
|
|
255
|
+
current += char;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
259
|
+
quoteChar = char;
|
|
260
|
+
current += char;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (char === ";") {
|
|
264
|
+
if (current.trim())
|
|
265
|
+
statements.push(current.trim());
|
|
266
|
+
current = "";
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
current += char;
|
|
270
|
+
}
|
|
271
|
+
if (current.trim())
|
|
272
|
+
statements.push(current.trim());
|
|
273
|
+
return statements;
|
|
274
|
+
}
|
|
275
|
+
/** DROP statement for a definition, for removePluginData. */
|
|
276
|
+
export function dropTableSql(dialect, pluginId, definition) {
|
|
277
|
+
const physical = prefixedTableName(pluginId, definition.name);
|
|
278
|
+
return `DROP TABLE IF EXISTS ${quote(dialect, physical)};`;
|
|
279
|
+
}
|
|
280
|
+
/** The ALTER that adds one newly declared column. */
|
|
281
|
+
export function addColumnSql(dialect, pluginId, definition, property) {
|
|
282
|
+
const physical = prefixedTableName(pluginId, definition.name);
|
|
283
|
+
const column = definition.columns[property];
|
|
284
|
+
const keyed = keyedColumns(definition).has(property);
|
|
285
|
+
const name = columnName(property, column);
|
|
286
|
+
let line = `${quote(dialect, name)} ${columnType(dialect, column, keyed)}`;
|
|
287
|
+
if (column.notNull)
|
|
288
|
+
line += " NOT NULL";
|
|
289
|
+
line += defaultClause(dialect, column);
|
|
290
|
+
return `ALTER TABLE ${quote(dialect, physical)} ADD COLUMN ${line};`;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Words and identifiers in one statement, with comments and string literals
|
|
294
|
+
* dropped. `"x"`, `` `x` `` and `[x]` all come out as the identifier x, so no
|
|
295
|
+
* quoting style hides a table name from the ownership check.
|
|
296
|
+
*/
|
|
297
|
+
function tokenize(source) {
|
|
298
|
+
const tokens = [];
|
|
299
|
+
let i = 0;
|
|
300
|
+
while (i < source.length) {
|
|
301
|
+
const char = source[i];
|
|
302
|
+
const next = source[i + 1];
|
|
303
|
+
if (/\s/.test(char)) {
|
|
304
|
+
i++;
|
|
305
|
+
}
|
|
306
|
+
else if (char === "-" && next === "-") {
|
|
307
|
+
while (i < source.length && source[i] !== "\n")
|
|
308
|
+
i++;
|
|
309
|
+
}
|
|
310
|
+
else if (char === "/" && next === "*") {
|
|
311
|
+
const end = source.indexOf("*/", i + 2);
|
|
312
|
+
i = end < 0 ? source.length : end + 2;
|
|
313
|
+
}
|
|
314
|
+
else if (char === "'") {
|
|
315
|
+
i++;
|
|
316
|
+
while (i < source.length) {
|
|
317
|
+
if (source[i] === "'" && source[i + 1] === "'")
|
|
318
|
+
i += 2;
|
|
319
|
+
else if (source[i] === "'")
|
|
320
|
+
break;
|
|
321
|
+
else
|
|
322
|
+
i++;
|
|
323
|
+
}
|
|
324
|
+
i++;
|
|
325
|
+
tokens.push({ value: "'", kind: "punct" });
|
|
326
|
+
}
|
|
327
|
+
else if (char === '"' || char === "`" || char === "[") {
|
|
328
|
+
const close = char === "[" ? "]" : char;
|
|
329
|
+
let value = "";
|
|
330
|
+
i++;
|
|
331
|
+
while (i < source.length) {
|
|
332
|
+
if (source[i] === close && source[i + 1] === close && close !== "]") {
|
|
333
|
+
value += close;
|
|
334
|
+
i += 2;
|
|
335
|
+
}
|
|
336
|
+
else if (source[i] === close) {
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
value += source[i++];
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
i++;
|
|
344
|
+
tokens.push({ value, kind: "quoted" });
|
|
345
|
+
}
|
|
346
|
+
else if (/[A-Za-z0-9_$]/.test(char)) {
|
|
347
|
+
let value = "";
|
|
348
|
+
while (i < source.length && /[A-Za-z0-9_$]/.test(source[i])) {
|
|
349
|
+
value += source[i++];
|
|
350
|
+
}
|
|
351
|
+
tokens.push({ value, kind: "word" });
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
tokens.push({ value: char, kind: "punct" });
|
|
355
|
+
i++;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return tokens;
|
|
359
|
+
}
|
|
360
|
+
/** Statements a plugin migration may never contain. */
|
|
361
|
+
const REFUSED_STATEMENTS = new Set([
|
|
362
|
+
"ATTACH",
|
|
363
|
+
"DETACH",
|
|
364
|
+
"PRAGMA",
|
|
365
|
+
"GRANT",
|
|
366
|
+
"REVOKE",
|
|
367
|
+
"COPY",
|
|
368
|
+
"LOAD",
|
|
369
|
+
"DO",
|
|
370
|
+
"CALL",
|
|
371
|
+
"HANDLER",
|
|
372
|
+
"WITH",
|
|
373
|
+
"SET",
|
|
374
|
+
"VACUUM",
|
|
375
|
+
]);
|
|
376
|
+
/** What a migration may CREATE, ALTER or DROP. Anything else is refused. */
|
|
377
|
+
const TABLE_OBJECTS = new Set(["TABLE", "VIEW"]);
|
|
378
|
+
/** Allowed, but only under the plugin's own prefix. */
|
|
379
|
+
const PREFIXED_OBJECTS = new Set(["SEQUENCE", "TYPE"]);
|
|
380
|
+
/**
|
|
381
|
+
* Functions that read files, change server settings or run a string as SQL.
|
|
382
|
+
* A SELECT calling one of them could do anything, so it is refused.
|
|
383
|
+
*/
|
|
384
|
+
const REFUSED_FUNCTIONS = new Set([
|
|
385
|
+
"SET_CONFIG",
|
|
386
|
+
"PG_READ_FILE",
|
|
387
|
+
"PG_READ_BINARY_FILE",
|
|
388
|
+
"PG_LS_DIR",
|
|
389
|
+
"PG_STAT_FILE",
|
|
390
|
+
"PG_FILE_WRITE",
|
|
391
|
+
"LO_IMPORT",
|
|
392
|
+
"LO_EXPORT",
|
|
393
|
+
"LO_FROM_BYTEA",
|
|
394
|
+
"DBLINK",
|
|
395
|
+
"DBLINK_EXEC",
|
|
396
|
+
"QUERY_TO_XML",
|
|
397
|
+
"QUERY_TO_XML_AND_XMLSCHEMA",
|
|
398
|
+
"PG_TERMINATE_BACKEND",
|
|
399
|
+
"PG_CANCEL_BACKEND",
|
|
400
|
+
"PG_RELOAD_CONF",
|
|
401
|
+
"LOAD_FILE",
|
|
402
|
+
"LOAD_EXTENSION",
|
|
403
|
+
"SYS_EXEC",
|
|
404
|
+
"SYS_EVAL",
|
|
405
|
+
]);
|
|
406
|
+
const CREATE_MODIFIERS = [
|
|
407
|
+
"OR",
|
|
408
|
+
"REPLACE",
|
|
409
|
+
"TEMP",
|
|
410
|
+
"TEMPORARY",
|
|
411
|
+
"UNLOGGED",
|
|
412
|
+
"GLOBAL",
|
|
413
|
+
"LOCAL",
|
|
414
|
+
"VIRTUAL",
|
|
415
|
+
"UNIQUE",
|
|
416
|
+
"CONCURRENTLY",
|
|
417
|
+
];
|
|
418
|
+
/**
|
|
419
|
+
* Every problem with the tables a migration writes to.
|
|
420
|
+
*
|
|
421
|
+
* The check has to hold against a plugin that is trying, not only one that
|
|
422
|
+
* made a typo, so it reads each statement the way the engine does rather
|
|
423
|
+
* than matching one pattern: a comment between a keyword and a name, bracket
|
|
424
|
+
* quoting, a second name in a DROP list, a schema-qualified name and
|
|
425
|
+
* `ALTER ... RENAME TO users` are all seen. Reads (FROM, JOIN, REFERENCES)
|
|
426
|
+
* are allowed; writes outside the plugin's prefix are not.
|
|
427
|
+
*
|
|
428
|
+
* `ownedLegacy` lists the legacy core tables this plugin adopts.
|
|
429
|
+
*/
|
|
430
|
+
export function findUnownedTableWrites(pluginId, sql, ownedLegacy = new Set(), ownedIndexes = new Set()) {
|
|
431
|
+
const prefix = tablePrefix(pluginId);
|
|
432
|
+
const problems = [];
|
|
433
|
+
const indexes = new Set([
|
|
434
|
+
...ownedIndexes,
|
|
435
|
+
...collectOwnedIndexes(pluginId, [sql], ownedLegacy),
|
|
436
|
+
]);
|
|
437
|
+
// The splitter and tokenizer follow standard quoting only. MySQL backslash
|
|
438
|
+
// escapes, E'' strings and Postgres dollar quoting would let a string end
|
|
439
|
+
// somewhere else for the engine than for this check, so none are allowed.
|
|
440
|
+
if (sql.includes("\\")) {
|
|
441
|
+
problems.push("backslashes are not allowed in a plugin migration");
|
|
442
|
+
}
|
|
443
|
+
if (/\$[A-Za-z_0-9]*\$/.test(sql)) {
|
|
444
|
+
problems.push("dollar-quoted strings are not allowed in a plugin migration");
|
|
445
|
+
}
|
|
446
|
+
if (problems.length > 0)
|
|
447
|
+
return problems;
|
|
448
|
+
for (const statement of splitStatements(sql)) {
|
|
449
|
+
const tokens = tokenize(statement);
|
|
450
|
+
if (tokens.length === 0)
|
|
451
|
+
continue;
|
|
452
|
+
const upper = (at) => tokens[at]?.kind === "word" ? tokens[at].value.toUpperCase() : "";
|
|
453
|
+
/** Checks the table name at `at`, returns the index after it. */
|
|
454
|
+
const table = (at) => {
|
|
455
|
+
const name = tokens[at];
|
|
456
|
+
if (!name || name.kind === "punct") {
|
|
457
|
+
problems.push("a statement writes to a table with no name");
|
|
458
|
+
return at + 1;
|
|
459
|
+
}
|
|
460
|
+
if (tokens[at + 1]?.value === ".") {
|
|
461
|
+
problems.push(`"${name.value}.${tokens[at + 2]?.value ?? ""}" names a schema; a plugin may only write its own tables`);
|
|
462
|
+
return at + 3;
|
|
463
|
+
}
|
|
464
|
+
if (!name.value.startsWith(prefix) && !ownedLegacy.has(name.value)) {
|
|
465
|
+
problems.push(`writes to "${name.value}", which is not prefixed "${prefix}"`);
|
|
466
|
+
}
|
|
467
|
+
return at + 1;
|
|
468
|
+
};
|
|
469
|
+
const skip = (at, words) => {
|
|
470
|
+
while (words.includes(upper(at)))
|
|
471
|
+
at++;
|
|
472
|
+
return at;
|
|
473
|
+
};
|
|
474
|
+
const find = (word, from = 0) => {
|
|
475
|
+
for (let at = from; at < tokens.length; at++) {
|
|
476
|
+
if (upper(at) === word)
|
|
477
|
+
return at;
|
|
478
|
+
}
|
|
479
|
+
return -1;
|
|
480
|
+
};
|
|
481
|
+
const first = upper(0);
|
|
482
|
+
if (REFUSED_STATEMENTS.has(first)) {
|
|
483
|
+
problems.push(`${first} is not allowed in a plugin migration`);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (first !== "CREATE" && find("CASCADE") > 0) {
|
|
487
|
+
problems.push("CASCADE is not allowed in a plugin migration");
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (first === "CREATE" || first === "ALTER" || first === "DROP") {
|
|
491
|
+
let at = skip(1, CREATE_MODIFIERS);
|
|
492
|
+
const object = upper(at);
|
|
493
|
+
if (object === "INDEX") {
|
|
494
|
+
// The index name is free; the table after ON is what it writes to.
|
|
495
|
+
const on = find("ON", at);
|
|
496
|
+
if (on > 0) {
|
|
497
|
+
table(skip(on + 1, ["ONLY"]));
|
|
498
|
+
}
|
|
499
|
+
else if (first === "CREATE") {
|
|
500
|
+
problems.push("CREATE INDEX without ON is not allowed");
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
// DROP INDEX name: only an index this plugin created.
|
|
504
|
+
const nameAt = skip(at + 1, ["IF", "EXISTS", "CONCURRENTLY"]);
|
|
505
|
+
const name = tokens[nameAt];
|
|
506
|
+
if (!name ||
|
|
507
|
+
name.kind === "punct" ||
|
|
508
|
+
tokens[nameAt + 1]?.value === "." ||
|
|
509
|
+
(!name.value.startsWith(prefix) && !indexes.has(name.value))) {
|
|
510
|
+
problems.push(`${first} INDEX "${name?.value ?? ""}" is not an index this plugin created`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (PREFIXED_OBJECTS.has(object)) {
|
|
516
|
+
const name = tokens[skip(at + 1, ["IF", "NOT", "EXISTS"])];
|
|
517
|
+
if (!name || !name.value.startsWith(prefix)) {
|
|
518
|
+
problems.push(`${first} ${object} "${name?.value ?? ""}" is not prefixed "${prefix}"`);
|
|
519
|
+
}
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (!TABLE_OBJECTS.has(object)) {
|
|
523
|
+
problems.push(`${first} ${object || tokens[at]?.value || ""} is not allowed in a plugin migration`.trim());
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
at = table(skip(at + 1, ["IF", "NOT", "EXISTS", "ONLY"]));
|
|
527
|
+
// DROP TABLE a, b drops both.
|
|
528
|
+
while (first === "DROP" && tokens[at]?.value === ",") {
|
|
529
|
+
at = table(at + 1);
|
|
530
|
+
}
|
|
531
|
+
// ALTER TABLE p_x RENAME TO users moves a table out of the namespace.
|
|
532
|
+
// RENAME [COLUMN] a TO b renames a column and is fine.
|
|
533
|
+
for (let rename = find("RENAME", at); rename > 0;) {
|
|
534
|
+
const next = upper(rename + 1);
|
|
535
|
+
if (next === "TO" || next === "AS")
|
|
536
|
+
table(rename + 2);
|
|
537
|
+
rename = find("RENAME", rename + 1);
|
|
538
|
+
}
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (first === "RENAME") {
|
|
542
|
+
// RENAME TABLE a TO b, c TO d
|
|
543
|
+
let at = skip(1, ["TABLE"]);
|
|
544
|
+
while (at < tokens.length) {
|
|
545
|
+
at = table(at);
|
|
546
|
+
if (upper(at) !== "TO")
|
|
547
|
+
break;
|
|
548
|
+
at = table(at + 1);
|
|
549
|
+
if (tokens[at]?.value !== ",")
|
|
550
|
+
break;
|
|
551
|
+
at++;
|
|
552
|
+
}
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (first === "TRUNCATE") {
|
|
556
|
+
let at = table(skip(1, ["TABLE", "ONLY"]));
|
|
557
|
+
while (tokens[at]?.value === ",")
|
|
558
|
+
at = table(at + 1);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (first === "INSERT" || first === "REPLACE") {
|
|
562
|
+
const into = find("INTO");
|
|
563
|
+
if (into > 0)
|
|
564
|
+
table(into + 1);
|
|
565
|
+
else
|
|
566
|
+
problems.push(`${first} without INTO is not allowed`);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (first === "UPDATE") {
|
|
570
|
+
// MySQL writes every table before SET: UPDATE a JOIN b ... SET b.x = 1.
|
|
571
|
+
const set = find("SET");
|
|
572
|
+
const end = set > 0 ? set : tokens.length;
|
|
573
|
+
for (let at = 1; at < end; at++) {
|
|
574
|
+
if (upper(at) === "JOIN" || tokens[at].value === ",") {
|
|
575
|
+
table(at + 1);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
table(skip(1, [
|
|
579
|
+
"OR",
|
|
580
|
+
"ROLLBACK",
|
|
581
|
+
"ABORT",
|
|
582
|
+
"REPLACE",
|
|
583
|
+
"FAIL",
|
|
584
|
+
"IGNORE",
|
|
585
|
+
"ONLY",
|
|
586
|
+
"LOW_PRIORITY",
|
|
587
|
+
]));
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (first === "DELETE") {
|
|
591
|
+
const from = find("FROM");
|
|
592
|
+
if (from < 0) {
|
|
593
|
+
problems.push("DELETE without FROM is not allowed");
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
// MySQL: DELETE a, b FROM ... deletes from a and b.
|
|
597
|
+
let at = skip(1, ["LOW_PRIORITY", "QUICK", "IGNORE"]);
|
|
598
|
+
if (at < from) {
|
|
599
|
+
while (at < from) {
|
|
600
|
+
at = table(at);
|
|
601
|
+
if (tokens[at]?.value !== ",")
|
|
602
|
+
break;
|
|
603
|
+
at++;
|
|
604
|
+
}
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
at = table(skip(from + 1, ["ONLY"]));
|
|
608
|
+
// DELETE FROM a, b USING ... deletes from both.
|
|
609
|
+
while (tokens[at]?.value === ",")
|
|
610
|
+
at = table(at + 1);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
if (first === "SELECT") {
|
|
614
|
+
// SELECT ... INTO creates a table on Postgres and writes a file on
|
|
615
|
+
// MySQL.
|
|
616
|
+
if (find("INTO") > 0) {
|
|
617
|
+
problems.push("SELECT INTO is not allowed in a plugin migration");
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
for (let at = 0; at < tokens.length; at++) {
|
|
621
|
+
if (REFUSED_FUNCTIONS.has(upper(at)) && tokens[at + 1]?.value === "(") {
|
|
622
|
+
problems.push(`${tokens[at].value}() is not allowed in a plugin migration`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// setval can break a core table's ids, so only the plugin's own.
|
|
626
|
+
for (const match of statement.matchAll(/setval\s*\(\s*([^,]*)/gi)) {
|
|
627
|
+
const target = /'([^']*)'/.exec(match[1])?.[1];
|
|
628
|
+
if (!target || !target.startsWith(prefix)) {
|
|
629
|
+
problems.push("setval() may only target this plugin's own sequences");
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (first !== "COMMENT") {
|
|
635
|
+
problems.push(`"${tokens[0].value}" statements are not allowed in a plugin migration`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return problems;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Names of the indexes a plugin's migrations create on its own tables, so a
|
|
642
|
+
* later DROP INDEX with no ON clause can be checked against them.
|
|
643
|
+
*/
|
|
644
|
+
export function collectOwnedIndexes(pluginId, sqls, ownedLegacy = new Set()) {
|
|
645
|
+
const prefix = tablePrefix(pluginId);
|
|
646
|
+
const owned = new Set();
|
|
647
|
+
for (const sql of sqls) {
|
|
648
|
+
for (const statement of splitStatements(sql)) {
|
|
649
|
+
const tokens = tokenize(statement);
|
|
650
|
+
const upper = (at) => tokens[at]?.kind === "word" ? tokens[at].value.toUpperCase() : "";
|
|
651
|
+
if (upper(0) !== "CREATE")
|
|
652
|
+
continue;
|
|
653
|
+
let at = 1;
|
|
654
|
+
while (CREATE_MODIFIERS.includes(upper(at)))
|
|
655
|
+
at++;
|
|
656
|
+
if (upper(at) !== "INDEX")
|
|
657
|
+
continue;
|
|
658
|
+
at++;
|
|
659
|
+
while (["IF", "NOT", "EXISTS"].includes(upper(at)))
|
|
660
|
+
at++;
|
|
661
|
+
const name = tokens[at];
|
|
662
|
+
let on = at;
|
|
663
|
+
while (on < tokens.length && upper(on) !== "ON")
|
|
664
|
+
on++;
|
|
665
|
+
if (upper(on + 1) === "ONLY")
|
|
666
|
+
on++;
|
|
667
|
+
const target = tokens[on + 1];
|
|
668
|
+
if (name &&
|
|
669
|
+
name.kind !== "punct" &&
|
|
670
|
+
target &&
|
|
671
|
+
tokens[on + 2]?.value !== "." &&
|
|
672
|
+
(target.value.startsWith(prefix) || ownedLegacy.has(target.value))) {
|
|
673
|
+
owned.add(name.value);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return owned;
|
|
678
|
+
}
|