@hot-updater/supabase 1.1.3 → 1.2.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/edge.cjs CHANGED
@@ -1,4 +1,4 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_supabaseStorage = require("./supabaseStorage-DAl3pyb7.cjs");
2
+ const require_supabaseStorage = require("./supabaseStorage-MqoRcaii.cjs");
3
3
  exports.supabaseDatabase = require_supabaseStorage.supabaseDatabase;
4
4
  exports.supabaseStorage = require_supabaseStorage.supabaseStorage;
package/dist/edge.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { n as supabaseDatabase, t as supabaseStorage } from "./supabaseStorage-C8xQ3nyM.mjs";
1
+ import { n as supabaseDatabase, t as supabaseStorage } from "./supabaseStorage-D4jcGa-l.mjs";
2
2
  export { supabaseDatabase, supabaseStorage };
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_supabaseStorage = require("./supabaseStorage-DAl3pyb7.cjs");
2
+ const require_supabaseStorage = require("./supabaseStorage-MqoRcaii.cjs");
3
3
  let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
4
4
  //#region src/supabaseSchema.ts
5
5
  /** Every table the apply RPC may name: models, index tables, settings, and the write guard. */
@@ -118,10 +118,20 @@ const supabaseSchemaStatements = ({ schema, settings: expected } = _hot_updater_
118
118
  ...settings
119
119
  ];
120
120
  };
121
- const supabaseSchemaSql = (target) => `-- HotUpdater.schema\n\n${supabaseSchemaStatements(target).map((statement) => `${statement};`).join("\n\n")}\n`;
121
+ const HEADER = "-- HotUpdater.schema\n\n";
122
+ const toSql = (statements) => `${HEADER}${statements.map((statement) => `${statement};`).join("\n\n")}\n`;
123
+ const supabaseSchemaSql = (target) => toSql(supabaseSchemaStatements(target));
122
124
  //#endregion
123
125
  //#region src/supabaseMigration.ts
124
126
  /**
127
+ * The resource this database reaches, without credentials, under the key
128
+ * `hot-updater doctor` reads to tell the database `hot-updater init` saved
129
+ * from another (a convention CLAUDE.md documents).
130
+ */
131
+ const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
132
+ /** `target` under {@link DATABASE_TARGET}, to spread into the database. */
133
+ const marked = (target) => ({ [DATABASE_TARGET]: target });
134
+ /**
125
135
  * Hot Updater's database on Supabase, with the migration `hot-updater db
126
136
  * generate` writes to `supabase/migrations`: core's tables, the server's
127
137
  * plugin tables, the apply RPC that may name them, and their settings rows.
@@ -129,6 +139,7 @@ const supabaseSchemaSql = (target) => `-- HotUpdater.schema\n\n${supabaseSchemaS
129
139
  */
130
140
  const supabaseDatabase = (config) => ({
131
141
  ...require_supabaseStorage.supabaseDatabase(config),
142
+ ...marked(`supabase/${new URL(config.supabaseUrl).hostname}`),
132
143
  generateSchema: ((version, _name, target = _hot_updater_plugin_core.coreTarget) => {
133
144
  if (version !== "latest" && version !== _hot_updater_plugin_core.coreSettings["schema.core"]) throw new Error(`Invalid version ${version}`);
134
145
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\D/g, "").slice(0, 14);
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as SUPABASE_TABLE_PREFIX, n as supabaseDatabase$1, r as SUPABASE_APPLY_FUNCTION, t as supabaseStorage } from "./supabaseStorage-C8xQ3nyM.mjs";
1
+ import { i as SUPABASE_TABLE_PREFIX, n as supabaseDatabase$1, r as SUPABASE_APPLY_FUNCTION, t as supabaseStorage } from "./supabaseStorage-D4jcGa-l.mjs";
2
2
  import { SETTINGS_TABLE, WRITE_GUARD_TABLE, coreSettings, coreTarget, createTableStatements, generateEngineSql, isMultiIndex } from "@hot-updater/plugin-core";
3
3
  //#region src/supabaseSchema.ts
4
4
  /** Every table the apply RPC may name: models, index tables, settings, and the write guard. */
@@ -117,10 +117,20 @@ const supabaseSchemaStatements = ({ schema, settings: expected } = coreTarget) =
117
117
  ...settings
118
118
  ];
119
119
  };
120
- const supabaseSchemaSql = (target) => `-- HotUpdater.schema\n\n${supabaseSchemaStatements(target).map((statement) => `${statement};`).join("\n\n")}\n`;
120
+ const HEADER = "-- HotUpdater.schema\n\n";
121
+ const toSql = (statements) => `${HEADER}${statements.map((statement) => `${statement};`).join("\n\n")}\n`;
122
+ const supabaseSchemaSql = (target) => toSql(supabaseSchemaStatements(target));
121
123
  //#endregion
122
124
  //#region src/supabaseMigration.ts
123
125
  /**
126
+ * The resource this database reaches, without credentials, under the key
127
+ * `hot-updater doctor` reads to tell the database `hot-updater init` saved
128
+ * from another (a convention CLAUDE.md documents).
129
+ */
130
+ const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
131
+ /** `target` under {@link DATABASE_TARGET}, to spread into the database. */
132
+ const marked = (target) => ({ [DATABASE_TARGET]: target });
133
+ /**
124
134
  * Hot Updater's database on Supabase, with the migration `hot-updater db
125
135
  * generate` writes to `supabase/migrations`: core's tables, the server's
126
136
  * plugin tables, the apply RPC that may name them, and their settings rows.
@@ -128,6 +138,7 @@ const supabaseSchemaSql = (target) => `-- HotUpdater.schema\n\n${supabaseSchemaS
128
138
  */
129
139
  const supabaseDatabase = (config) => ({
130
140
  ...supabaseDatabase$1(config),
141
+ ...marked(`supabase/${new URL(config.supabaseUrl).hostname}`),
131
142
  generateSchema: ((version, _name, target = coreTarget) => {
132
143
  if (version !== "latest" && version !== coreSettings["schema.core"]) throw new Error(`Invalid version ${version}`);
133
144
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\D/g, "").slice(0, 14);
@@ -6370,11 +6370,14 @@ const toApplyStatement = ({ sql, params }) => {
6370
6370
  const withCode = (error) => Object.assign(new Error(error.message, { cause: error }), { code: error.code === "PGRST202" || error.code === "PGRST205" ? "42P01" : error.code });
6371
6371
  /**
6372
6372
  * The SQL core's executor over Supabase: every statement goes through the
6373
- * apply RPC, and a write is one RPC call, so one transaction.
6373
+ * apply RPC, and a write is one RPC call, so one transaction. `client` may
6374
+ * be what creates the client, called on the first statement.
6374
6375
  */
6375
6376
  const supabaseExecutor = (client) => {
6377
+ let created;
6378
+ const clientOf = () => typeof client === "function" ? created ??= client() : client;
6376
6379
  const apply = async (statements) => {
6377
- const { data, error } = await client.rpc(SUPABASE_APPLY_FUNCTION, { p_statements: statements.map(toApplyStatement) });
6380
+ const { data, error } = await clientOf().rpc(SUPABASE_APPLY_FUNCTION, { p_statements: statements.map(toApplyStatement) });
6378
6381
  if (error) throw withCode(error);
6379
6382
  return data;
6380
6383
  };
@@ -6397,7 +6400,7 @@ const supabaseExecutor = (client) => {
6397
6400
  */
6398
6401
  const supabaseDatabase = (config) => {
6399
6402
  const { migrations: _migrations, ...adapter } = (0, _hot_updater_plugin_core.createSqlAdapter)({
6400
- executor: supabaseExecutor((0, _supabase_supabase_js.createClient)(config.supabaseUrl, resolveSupabaseServiceRoleKey(config))),
6403
+ executor: supabaseExecutor(() => (0, _supabase_supabase_js.createClient)(config.supabaseUrl, resolveSupabaseServiceRoleKey(config))),
6401
6404
  tablePrefix: SUPABASE_TABLE_PREFIX
6402
6405
  });
6403
6406
  return (0, _hot_updater_plugin_core.createEngineDatabase)({
@@ -6683,12 +6686,18 @@ const isSupabaseDatabaseAuthError = (err) => {
6683
6686
  const stderr = err.stderr;
6684
6687
  return typeof stderr === "string" && SUPABASE_DATABASE_AUTH_ERROR_PATTERNS.some((pattern) => pattern.test(stderr));
6685
6688
  };
6686
- const handleSupabaseDatabaseCommandError = (err, { dbPassword, stderrInherited = false }) => {
6689
+ /** What a failed database command reports: a bad password, or the CLI's output. */
6690
+ const supabaseDatabaseCommandErrorMessage = (err, { dbPassword, stderrInherited = false }) => {
6687
6691
  if (err instanceof ExecaError) {
6688
- if (dbPassword && isSupabaseDatabaseAuthError(err)) _hot_updater_cli_tools.p.log.error(SUPABASE_DATABASE_CONNECTION_ERROR);
6689
- else if (!stderrInherited && err.stderr) _hot_updater_cli_tools.p.log.error(err.stderr);
6690
- else _hot_updater_cli_tools.p.log.error(err.message);
6691
- } else console.error(err);
6692
+ if (dbPassword && isSupabaseDatabaseAuthError(err)) return SUPABASE_DATABASE_CONNECTION_ERROR;
6693
+ if (!stderrInherited && err.stderr) return err.stderr;
6694
+ return err.message;
6695
+ }
6696
+ return err instanceof Error ? err.message : String(err);
6697
+ };
6698
+ const handleSupabaseDatabaseCommandError = (err, options) => {
6699
+ if (err instanceof ExecaError) _hot_updater_cli_tools.p.log.error(supabaseDatabaseCommandErrorMessage(err, options));
6700
+ else console.error(err);
6692
6701
  process.exit(1);
6693
6702
  };
6694
6703
  const confirmSupabaseDatabaseMigrations = async ({ nonInteractive }) => {
@@ -6698,71 +6707,80 @@ const confirmSupabaseDatabaseMigrations = async ({ nonInteractive }) => {
6698
6707
  initialValue: true
6699
6708
  }) === true;
6700
6709
  };
6701
- const linkSupabase = async (workdir, { accessToken, projectId, dbPassword }) => {
6710
+ const runSupabaseLink = async (workdir, { accessToken, projectId, dbPassword }) => {
6711
+ await fs_promises.default.writeFile(path.default.join(workdir, "supabase", "config.toml"), (0, _hot_updater_cli_tools.transformTemplate)(SUPABASE_CONFIG_TEMPLATE, { projectId }));
6712
+ await execa("npx", [
6713
+ "supabase",
6714
+ "link",
6715
+ "--project-ref",
6716
+ projectId,
6717
+ "--workdir",
6718
+ workdir
6719
+ ], {
6720
+ cwd: workdir,
6721
+ env: getSupabaseCommandEnv(accessToken, dbPassword),
6722
+ input: "",
6723
+ stdio: [
6724
+ "pipe",
6725
+ "pipe",
6726
+ "pipe"
6727
+ ]
6728
+ });
6729
+ };
6730
+ const linkSupabase = async (workdir, options) => {
6702
6731
  const spinner = _hot_updater_cli_tools.p.spinner();
6703
6732
  try {
6704
- await fs_promises.default.writeFile(path.default.join(workdir, "supabase", "config.toml"), (0, _hot_updater_cli_tools.transformTemplate)(SUPABASE_CONFIG_TEMPLATE, { projectId }));
6705
6733
  spinner.start("Linking Supabase...");
6706
- await execa("npx", [
6707
- "supabase",
6708
- "link",
6709
- "--project-ref",
6710
- projectId,
6711
- "--workdir",
6712
- workdir
6713
- ], {
6714
- cwd: workdir,
6715
- env: getSupabaseCommandEnv(accessToken, dbPassword),
6716
- input: "",
6717
- stdio: [
6718
- "pipe",
6719
- "pipe",
6720
- "pipe"
6721
- ]
6722
- });
6734
+ await runSupabaseLink(workdir, options);
6723
6735
  spinner.stop("Supabase linked ✔");
6724
6736
  } catch (err) {
6725
6737
  spinner.stop();
6726
- handleSupabaseDatabaseCommandError(err instanceof Error ? err : new Error(String(err)), { dbPassword });
6738
+ handleSupabaseDatabaseCommandError(err instanceof Error ? err : new Error(String(err)), { dbPassword: options.dbPassword });
6727
6739
  }
6728
6740
  };
6729
- const pushDB = async (workdir, { accessToken, dbPassword }) => {
6730
- try {
6731
- await execa("npx", [
6732
- "supabase",
6733
- "migration",
6734
- "fetch",
6735
- "--linked",
6736
- "--yes",
6737
- "--workdir",
6738
- workdir
6739
- ], {
6740
- cwd: workdir,
6741
- env: getSupabaseCommandEnv(accessToken, dbPassword)
6742
- }).catch((error) => {
6743
- if (error instanceof ExecaError && [error.stderr, error.stdout].some((output) => typeof output === "string" && /relation \\?"supabase_migrations\.schema_migrations\\?" does not exist/.test(output))) return;
6744
- throw error;
6745
- });
6746
- const dbPush = await execa("npx", [
6747
- "supabase",
6748
- "db",
6749
- "push",
6750
- "--include-all",
6751
- "--yes",
6752
- "--workdir",
6753
- workdir
6754
- ], {
6755
- cwd: workdir,
6756
- env: getSupabaseCommandEnv(accessToken, dbPassword),
6741
+ /** Pushes the workdir's migrations the linked project has not run, after its own history. */
6742
+ const runSupabasePush = async (workdir, { accessToken, dbPassword, quiet = false }) => {
6743
+ await execa("npx", [
6744
+ "supabase",
6745
+ "migration",
6746
+ "fetch",
6747
+ "--linked",
6748
+ "--yes",
6749
+ "--workdir",
6750
+ workdir
6751
+ ], {
6752
+ cwd: workdir,
6753
+ env: getSupabaseCommandEnv(accessToken, dbPassword)
6754
+ }).catch((error) => {
6755
+ if (error instanceof ExecaError && [error.stderr, error.stdout].some((output) => typeof output === "string" && /relation \\?"supabase_migrations\.schema_migrations\\?" does not exist/.test(output))) return;
6756
+ throw error;
6757
+ });
6758
+ return execa("npx", [
6759
+ "supabase",
6760
+ "db",
6761
+ "push",
6762
+ "--include-all",
6763
+ "--yes",
6764
+ "--workdir",
6765
+ workdir
6766
+ ], {
6767
+ cwd: workdir,
6768
+ env: getSupabaseCommandEnv(accessToken, dbPassword),
6769
+ ...quiet ? { input: "" } : {
6757
6770
  stderr: ["pipe", "inherit"],
6758
6771
  stdin: "inherit",
6759
6772
  stdout: "inherit"
6760
- });
6773
+ }
6774
+ });
6775
+ };
6776
+ const pushDB = async (workdir, options) => {
6777
+ try {
6778
+ const dbPush = await runSupabasePush(workdir, options);
6761
6779
  _hot_updater_cli_tools.p.log.success("DB pushed ✔");
6762
6780
  return dbPush.stdout;
6763
6781
  } catch (err) {
6764
6782
  handleSupabaseDatabaseCommandError(err instanceof Error ? err : new Error(String(err)), {
6765
- dbPassword,
6783
+ dbPassword: options.dbPassword,
6766
6784
  stderrInherited: true
6767
6785
  });
6768
6786
  }
@@ -6365,11 +6365,14 @@ const toApplyStatement = ({ sql, params }) => {
6365
6365
  const withCode = (error) => Object.assign(new Error(error.message, { cause: error }), { code: error.code === "PGRST202" || error.code === "PGRST205" ? "42P01" : error.code });
6366
6366
  /**
6367
6367
  * The SQL core's executor over Supabase: every statement goes through the
6368
- * apply RPC, and a write is one RPC call, so one transaction.
6368
+ * apply RPC, and a write is one RPC call, so one transaction. `client` may
6369
+ * be what creates the client, called on the first statement.
6369
6370
  */
6370
6371
  const supabaseExecutor = (client) => {
6372
+ let created;
6373
+ const clientOf = () => typeof client === "function" ? created ??= client() : client;
6371
6374
  const apply = async (statements) => {
6372
- const { data, error } = await client.rpc(SUPABASE_APPLY_FUNCTION, { p_statements: statements.map(toApplyStatement) });
6375
+ const { data, error } = await clientOf().rpc(SUPABASE_APPLY_FUNCTION, { p_statements: statements.map(toApplyStatement) });
6373
6376
  if (error) throw withCode(error);
6374
6377
  return data;
6375
6378
  };
@@ -6392,7 +6395,7 @@ const supabaseExecutor = (client) => {
6392
6395
  */
6393
6396
  const supabaseDatabase = (config) => {
6394
6397
  const { migrations: _migrations, ...adapter } = createSqlAdapter({
6395
- executor: supabaseExecutor(createClient(config.supabaseUrl, resolveSupabaseServiceRoleKey(config))),
6398
+ executor: supabaseExecutor(() => createClient(config.supabaseUrl, resolveSupabaseServiceRoleKey(config))),
6396
6399
  tablePrefix: SUPABASE_TABLE_PREFIX
6397
6400
  });
6398
6401
  return createEngineDatabase({
@@ -6678,12 +6681,18 @@ const isSupabaseDatabaseAuthError = (err) => {
6678
6681
  const stderr = err.stderr;
6679
6682
  return typeof stderr === "string" && SUPABASE_DATABASE_AUTH_ERROR_PATTERNS.some((pattern) => pattern.test(stderr));
6680
6683
  };
6681
- const handleSupabaseDatabaseCommandError = (err, { dbPassword, stderrInherited = false }) => {
6684
+ /** What a failed database command reports: a bad password, or the CLI's output. */
6685
+ const supabaseDatabaseCommandErrorMessage = (err, { dbPassword, stderrInherited = false }) => {
6682
6686
  if (err instanceof ExecaError) {
6683
- if (dbPassword && isSupabaseDatabaseAuthError(err)) p.log.error(SUPABASE_DATABASE_CONNECTION_ERROR);
6684
- else if (!stderrInherited && err.stderr) p.log.error(err.stderr);
6685
- else p.log.error(err.message);
6686
- } else console.error(err);
6687
+ if (dbPassword && isSupabaseDatabaseAuthError(err)) return SUPABASE_DATABASE_CONNECTION_ERROR;
6688
+ if (!stderrInherited && err.stderr) return err.stderr;
6689
+ return err.message;
6690
+ }
6691
+ return err instanceof Error ? err.message : String(err);
6692
+ };
6693
+ const handleSupabaseDatabaseCommandError = (err, options) => {
6694
+ if (err instanceof ExecaError) p.log.error(supabaseDatabaseCommandErrorMessage(err, options));
6695
+ else console.error(err);
6687
6696
  process.exit(1);
6688
6697
  };
6689
6698
  const confirmSupabaseDatabaseMigrations = async ({ nonInteractive }) => {
@@ -6693,71 +6702,80 @@ const confirmSupabaseDatabaseMigrations = async ({ nonInteractive }) => {
6693
6702
  initialValue: true
6694
6703
  }) === true;
6695
6704
  };
6696
- const linkSupabase = async (workdir, { accessToken, projectId, dbPassword }) => {
6705
+ const runSupabaseLink = async (workdir, { accessToken, projectId, dbPassword }) => {
6706
+ await fs.writeFile(path.join(workdir, "supabase", "config.toml"), transformTemplate(SUPABASE_CONFIG_TEMPLATE, { projectId }));
6707
+ await execa("npx", [
6708
+ "supabase",
6709
+ "link",
6710
+ "--project-ref",
6711
+ projectId,
6712
+ "--workdir",
6713
+ workdir
6714
+ ], {
6715
+ cwd: workdir,
6716
+ env: getSupabaseCommandEnv(accessToken, dbPassword),
6717
+ input: "",
6718
+ stdio: [
6719
+ "pipe",
6720
+ "pipe",
6721
+ "pipe"
6722
+ ]
6723
+ });
6724
+ };
6725
+ const linkSupabase = async (workdir, options) => {
6697
6726
  const spinner = p.spinner();
6698
6727
  try {
6699
- await fs.writeFile(path.join(workdir, "supabase", "config.toml"), transformTemplate(SUPABASE_CONFIG_TEMPLATE, { projectId }));
6700
6728
  spinner.start("Linking Supabase...");
6701
- await execa("npx", [
6702
- "supabase",
6703
- "link",
6704
- "--project-ref",
6705
- projectId,
6706
- "--workdir",
6707
- workdir
6708
- ], {
6709
- cwd: workdir,
6710
- env: getSupabaseCommandEnv(accessToken, dbPassword),
6711
- input: "",
6712
- stdio: [
6713
- "pipe",
6714
- "pipe",
6715
- "pipe"
6716
- ]
6717
- });
6729
+ await runSupabaseLink(workdir, options);
6718
6730
  spinner.stop("Supabase linked ✔");
6719
6731
  } catch (err) {
6720
6732
  spinner.stop();
6721
- handleSupabaseDatabaseCommandError(err instanceof Error ? err : new Error(String(err)), { dbPassword });
6733
+ handleSupabaseDatabaseCommandError(err instanceof Error ? err : new Error(String(err)), { dbPassword: options.dbPassword });
6722
6734
  }
6723
6735
  };
6724
- const pushDB = async (workdir, { accessToken, dbPassword }) => {
6725
- try {
6726
- await execa("npx", [
6727
- "supabase",
6728
- "migration",
6729
- "fetch",
6730
- "--linked",
6731
- "--yes",
6732
- "--workdir",
6733
- workdir
6734
- ], {
6735
- cwd: workdir,
6736
- env: getSupabaseCommandEnv(accessToken, dbPassword)
6737
- }).catch((error) => {
6738
- if (error instanceof ExecaError && [error.stderr, error.stdout].some((output) => typeof output === "string" && /relation \\?"supabase_migrations\.schema_migrations\\?" does not exist/.test(output))) return;
6739
- throw error;
6740
- });
6741
- const dbPush = await execa("npx", [
6742
- "supabase",
6743
- "db",
6744
- "push",
6745
- "--include-all",
6746
- "--yes",
6747
- "--workdir",
6748
- workdir
6749
- ], {
6750
- cwd: workdir,
6751
- env: getSupabaseCommandEnv(accessToken, dbPassword),
6736
+ /** Pushes the workdir's migrations the linked project has not run, after its own history. */
6737
+ const runSupabasePush = async (workdir, { accessToken, dbPassword, quiet = false }) => {
6738
+ await execa("npx", [
6739
+ "supabase",
6740
+ "migration",
6741
+ "fetch",
6742
+ "--linked",
6743
+ "--yes",
6744
+ "--workdir",
6745
+ workdir
6746
+ ], {
6747
+ cwd: workdir,
6748
+ env: getSupabaseCommandEnv(accessToken, dbPassword)
6749
+ }).catch((error) => {
6750
+ if (error instanceof ExecaError && [error.stderr, error.stdout].some((output) => typeof output === "string" && /relation \\?"supabase_migrations\.schema_migrations\\?" does not exist/.test(output))) return;
6751
+ throw error;
6752
+ });
6753
+ return execa("npx", [
6754
+ "supabase",
6755
+ "db",
6756
+ "push",
6757
+ "--include-all",
6758
+ "--yes",
6759
+ "--workdir",
6760
+ workdir
6761
+ ], {
6762
+ cwd: workdir,
6763
+ env: getSupabaseCommandEnv(accessToken, dbPassword),
6764
+ ...quiet ? { input: "" } : {
6752
6765
  stderr: ["pipe", "inherit"],
6753
6766
  stdin: "inherit",
6754
6767
  stdout: "inherit"
6755
- });
6768
+ }
6769
+ });
6770
+ };
6771
+ const pushDB = async (workdir, options) => {
6772
+ try {
6773
+ const dbPush = await runSupabasePush(workdir, options);
6756
6774
  p.log.success("DB pushed ✔");
6757
6775
  return dbPush.stdout;
6758
6776
  } catch (err) {
6759
6777
  handleSupabaseDatabaseCommandError(err instanceof Error ? err : new Error(String(err)), {
6760
- dbPassword,
6778
+ dbPassword: options.dbPassword,
6761
6779
  stderrInherited: true
6762
6780
  });
6763
6781
  }
@@ -37,11 +37,14 @@ const toApplyStatement = ({ sql, params }) => {
37
37
  const withCode = (error) => Object.assign(new Error(error.message, { cause: error }), { code: error.code === "PGRST202" || error.code === "PGRST205" ? "42P01" : error.code });
38
38
  /**
39
39
  * The SQL core's executor over Supabase: every statement goes through the
40
- * apply RPC, and a write is one RPC call, so one transaction.
40
+ * apply RPC, and a write is one RPC call, so one transaction. `client` may
41
+ * be what creates the client, called on the first statement.
41
42
  */
42
43
  const supabaseExecutor = (client) => {
44
+ let created;
45
+ const clientOf = () => typeof client === "function" ? created ??= client() : client;
43
46
  const apply = async (statements) => {
44
- const { data, error } = await client.rpc(SUPABASE_APPLY_FUNCTION, { p_statements: statements.map(toApplyStatement) });
47
+ const { data, error } = await clientOf().rpc(SUPABASE_APPLY_FUNCTION, { p_statements: statements.map(toApplyStatement) });
45
48
  if (error) throw withCode(error);
46
49
  return data;
47
50
  };
@@ -64,7 +67,7 @@ const supabaseExecutor = (client) => {
64
67
  */
65
68
  const supabaseDatabase = (config) => {
66
69
  const { migrations: _migrations, ...adapter } = createSqlAdapter({
67
- executor: supabaseExecutor(createClient(config.supabaseUrl, resolveSupabaseServiceRoleKey(config))),
70
+ executor: supabaseExecutor(() => createClient(config.supabaseUrl, resolveSupabaseServiceRoleKey(config))),
68
71
  tablePrefix: SUPABASE_TABLE_PREFIX
69
72
  });
70
73
  return createEngineDatabase({
@@ -166,10 +169,11 @@ const utf8 = new TextEncoder();
166
169
  */
167
170
  const toObjectName = (key) => key.replace(ESCAPED_CHARACTER, (character) => Array.from(utf8.encode(character), (byte) => `!${byte.toString(16).toUpperCase().padStart(2, "0")}`).join(""));
168
171
  const supabaseStorage = (config) => {
169
- const bucket = createClient(config.supabaseUrl, resolveSupabaseServiceRoleKey(config)).storage.from(config.bucketName);
172
+ let client;
173
+ const bucket = () => (client ??= createClient(config.supabaseUrl, resolveSupabaseServiceRoleKey(config))).storage.from(config.bucketName);
170
174
  const getStorageKey = createStorageKeyBuilder(config.basePath);
171
175
  const resolveSignedUrl = createSupabaseSignedUrlBatcher({
172
- createSignedUrls: (_bucketName, keys, expiresIn) => bucket.createSignedUrls(keys, expiresIn),
176
+ createSignedUrls: (_bucketName, keys, expiresIn) => bucket().createSignedUrls(keys, expiresIn),
173
177
  expiresIn: config.signedUrlExpiresIn ?? 3600,
174
178
  formatObjectPath: (_bucketName, key) => key
175
179
  });
@@ -183,7 +187,7 @@ const supabaseStorage = (config) => {
183
187
  protocol: "supabase-storage",
184
188
  async put({ key, body, contentLength, contentType }) {
185
189
  const storageKey = getStorageKey(key);
186
- const { error } = await bucket.upload(toObjectName(storageKey), body, {
190
+ const { error } = await bucket().upload(toObjectName(storageKey), body, {
187
191
  contentType,
188
192
  cacheControl: "max-age=31536000",
189
193
  upsert: true,
@@ -199,7 +203,7 @@ const supabaseStorage = (config) => {
199
203
  },
200
204
  async get({ storageUri }) {
201
205
  const { key } = parseAndValidate(storageUri);
202
- const { data, error } = await bucket.download(toObjectName(key));
206
+ const { data, error } = await bucket().download(toObjectName(key));
203
207
  if (error) {
204
208
  if (await isMissingObjectError(error)) return { response: null };
205
209
  throw new Error(`Failed to download storage object: ${error.message}`);
@@ -212,7 +216,7 @@ const supabaseStorage = (config) => {
212
216
  },
213
217
  async exists({ storageUri }) {
214
218
  const { key } = parseAndValidate(storageUri);
215
- const { data, error } = await bucket.exists(toObjectName(key));
219
+ const { data, error } = await bucket().exists(toObjectName(key));
216
220
  if (error) {
217
221
  if (isMissingExistsError(error)) return { exists: false };
218
222
  throw error;
@@ -221,7 +225,7 @@ const supabaseStorage = (config) => {
221
225
  },
222
226
  async delete({ storageUri }) {
223
227
  const { key } = parseAndValidate(storageUri);
224
- const { error } = await bucket.remove([toObjectName(key)]);
228
+ const { error } = await bucket().remove([toObjectName(key)]);
225
229
  if (error && !isNotFoundError(error)) throw new Error(`Failed to delete storage object: ${error.message}`);
226
230
  return { deleted: true };
227
231
  }
@@ -37,11 +37,14 @@ const toApplyStatement = ({ sql, params }) => {
37
37
  const withCode = (error) => Object.assign(new Error(error.message, { cause: error }), { code: error.code === "PGRST202" || error.code === "PGRST205" ? "42P01" : error.code });
38
38
  /**
39
39
  * The SQL core's executor over Supabase: every statement goes through the
40
- * apply RPC, and a write is one RPC call, so one transaction.
40
+ * apply RPC, and a write is one RPC call, so one transaction. `client` may
41
+ * be what creates the client, called on the first statement.
41
42
  */
42
43
  const supabaseExecutor = (client) => {
44
+ let created;
45
+ const clientOf = () => typeof client === "function" ? created ??= client() : client;
43
46
  const apply = async (statements) => {
44
- const { data, error } = await client.rpc(SUPABASE_APPLY_FUNCTION, { p_statements: statements.map(toApplyStatement) });
47
+ const { data, error } = await clientOf().rpc(SUPABASE_APPLY_FUNCTION, { p_statements: statements.map(toApplyStatement) });
45
48
  if (error) throw withCode(error);
46
49
  return data;
47
50
  };
@@ -64,7 +67,7 @@ const supabaseExecutor = (client) => {
64
67
  */
65
68
  const supabaseDatabase = (config) => {
66
69
  const { migrations: _migrations, ...adapter } = (0, _hot_updater_plugin_core.createSqlAdapter)({
67
- executor: supabaseExecutor((0, _supabase_supabase_js.createClient)(config.supabaseUrl, resolveSupabaseServiceRoleKey(config))),
70
+ executor: supabaseExecutor(() => (0, _supabase_supabase_js.createClient)(config.supabaseUrl, resolveSupabaseServiceRoleKey(config))),
68
71
  tablePrefix: SUPABASE_TABLE_PREFIX
69
72
  });
70
73
  return (0, _hot_updater_plugin_core.createEngineDatabase)({
@@ -166,10 +169,11 @@ const utf8 = new TextEncoder();
166
169
  */
167
170
  const toObjectName = (key) => key.replace(ESCAPED_CHARACTER, (character) => Array.from(utf8.encode(character), (byte) => `!${byte.toString(16).toUpperCase().padStart(2, "0")}`).join(""));
168
171
  const supabaseStorage = (config) => {
169
- const bucket = (0, _supabase_supabase_js.createClient)(config.supabaseUrl, resolveSupabaseServiceRoleKey(config)).storage.from(config.bucketName);
172
+ let client;
173
+ const bucket = () => (client ??= (0, _supabase_supabase_js.createClient)(config.supabaseUrl, resolveSupabaseServiceRoleKey(config))).storage.from(config.bucketName);
170
174
  const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
171
175
  const resolveSignedUrl = createSupabaseSignedUrlBatcher({
172
- createSignedUrls: (_bucketName, keys, expiresIn) => bucket.createSignedUrls(keys, expiresIn),
176
+ createSignedUrls: (_bucketName, keys, expiresIn) => bucket().createSignedUrls(keys, expiresIn),
173
177
  expiresIn: config.signedUrlExpiresIn ?? 3600,
174
178
  formatObjectPath: (_bucketName, key) => key
175
179
  });
@@ -183,7 +187,7 @@ const supabaseStorage = (config) => {
183
187
  protocol: "supabase-storage",
184
188
  async put({ key, body, contentLength, contentType }) {
185
189
  const storageKey = getStorageKey(key);
186
- const { error } = await bucket.upload(toObjectName(storageKey), body, {
190
+ const { error } = await bucket().upload(toObjectName(storageKey), body, {
187
191
  contentType,
188
192
  cacheControl: "max-age=31536000",
189
193
  upsert: true,
@@ -199,7 +203,7 @@ const supabaseStorage = (config) => {
199
203
  },
200
204
  async get({ storageUri }) {
201
205
  const { key } = parseAndValidate(storageUri);
202
- const { data, error } = await bucket.download(toObjectName(key));
206
+ const { data, error } = await bucket().download(toObjectName(key));
203
207
  if (error) {
204
208
  if (await isMissingObjectError(error)) return { response: null };
205
209
  throw new Error(`Failed to download storage object: ${error.message}`);
@@ -212,7 +216,7 @@ const supabaseStorage = (config) => {
212
216
  },
213
217
  async exists({ storageUri }) {
214
218
  const { key } = parseAndValidate(storageUri);
215
- const { data, error } = await bucket.exists(toObjectName(key));
219
+ const { data, error } = await bucket().exists(toObjectName(key));
216
220
  if (error) {
217
221
  if (isMissingExistsError(error)) return { exists: false };
218
222
  throw error;
@@ -221,7 +225,7 @@ const supabaseStorage = (config) => {
221
225
  },
222
226
  async delete({ storageUri }) {
223
227
  const { key } = parseAndValidate(storageUri);
224
- const { error } = await bucket.remove([toObjectName(key)]);
228
+ const { error } = await bucket().remove([toObjectName(key)]);
225
229
  if (error && !isNotFoundError(error)) throw new Error(`Failed to delete storage object: ${error.message}`);
226
230
  return { deleted: true };
227
231
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/supabase",
3
3
  "type": "module",
4
- "version": "1.1.3",
4
+ "version": "1.2.0-rc.0",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.mjs",
@@ -51,10 +51,10 @@
51
51
  "@supabase/supabase-js": "2.76.1",
52
52
  "hono": "4.12.34",
53
53
  "uuidv7": "^1.0.2",
54
- "@hot-updater/protocol": "1.1.3",
55
- "@hot-updater/plugin-core": "1.1.3",
56
- "@hot-updater/server": "1.1.3",
57
- "@hot-updater/cli-tools": "1.1.3"
54
+ "@hot-updater/protocol": "1.2.0-rc.0",
55
+ "@hot-updater/plugin-core": "1.2.0-rc.0",
56
+ "@hot-updater/cli-tools": "1.2.0-rc.0",
57
+ "@hot-updater/server": "1.2.0-rc.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@electric-sql/pglite": "0.4.1",
@@ -64,9 +64,9 @@
64
64
  "execa": "9.5.2",
65
65
  "@types/node": "^20",
66
66
  "mime": "^4.0.4",
67
- "@hot-updater/mock": "1.1.3",
68
- "@hot-updater/test-utils": "1.1.3",
69
- "@hot-updater/postgres": "1.1.3"
67
+ "@hot-updater/mock": "1.2.0-rc.0",
68
+ "@hot-updater/test-utils": "1.2.0-rc.0",
69
+ "@hot-updater/postgres": "1.2.0-rc.0"
70
70
  },
71
71
  "scripts": {
72
72
  "build": "tsdown",
@@ -0,0 +1,73 @@
1
+ -- HotUpdater.schema
2
+
3
+ CREATE TABLE IF NOT EXISTS "hot_updater_v1_native_builds" ("id" varchar(36) COLLATE "C" NOT NULL, "scope_key" varchar(2048) COLLATE "C" NOT NULL, "platform" varchar(16) COLLATE "C" NOT NULL, "channel_id" varchar(1408) COLLATE "C" NOT NULL, "fingerprint_hash" varchar(255) COLLATE "C", "app_version" text COLLATE "C", "min_bundle_id" varchar(36) COLLATE "C" NOT NULL, "artifact_kind" varchar(16) COLLATE "C" NOT NULL, "artifact_storage_uri" text COLLATE "C" NOT NULL, "byte_size" bigint NOT NULL, "git_commit_hash" varchar(255) COLLATE "C", "created_at_ms" bigint NOT NULL, "build_key" varchar(69) COLLATE "C", "build_number" varchar(64) COLLATE "C", "submitted_to" varchar(16) COLLATE "C", "submitted_at_ms" bigint, "submitted_version" varchar(255) COLLATE "C", "_v" bigint NOT NULL DEFAULT 0, PRIMARY KEY ("id"));
4
+
5
+ CREATE INDEX IF NOT EXISTS "hot_updater_v1_native_builds_byScope" ON "hot_updater_v1_native_builds" ("scope_key", "id");
6
+
7
+ CREATE INDEX IF NOT EXISTS "hot_updater_v1_native_builds_byBuildKey" ON "hot_updater_v1_native_builds" ("scope_key", "build_key", "id");
8
+
9
+ CREATE INDEX IF NOT EXISTS "hot_updater_v1_native_builds_byChannel" ON "hot_updater_v1_native_builds" ("channel_id", "id");
10
+
11
+ CREATE UNIQUE INDEX IF NOT EXISTS "hot_updater_v1_native_builds_min_bundle_id" ON "hot_updater_v1_native_builds" ("min_bundle_id");
12
+
13
+ ALTER TABLE "hot_updater_v1_native_builds" ENABLE ROW LEVEL SECURITY;
14
+
15
+ CREATE OR REPLACE FUNCTION public.hot_updater_v1_apply(p_statements jsonb)
16
+ RETURNS jsonb
17
+ LANGUAGE plpgsql
18
+ SECURITY INVOKER
19
+ SET search_path = public, pg_temp
20
+ AS $apply$
21
+ DECLARE
22
+ item jsonb;
23
+ statement text;
24
+ shape text;
25
+ target text;
26
+ found record;
27
+ rows jsonb;
28
+ changed bigint;
29
+ results jsonb := '[]'::jsonb;
30
+ BEGIN
31
+ FOR item IN SELECT value FROM jsonb_array_elements(p_statements) LOOP
32
+ statement := item->>'sql';
33
+ FOR target IN SELECT (regexp_matches(statement, '(?:FROM|INTO|UPDATE|JOIN) "([^"]+)"', 'g'))[1] LOOP
34
+ IF NOT target = ANY (ARRAY['hot_updater_v1_bundles', 'hot_updater_v1_bundle_patches', 'hot_updater_v1_releases', 'hot_updater_v1_release_catalogs', 'hot_updater_v1_channels', 'hot_updater_v1_bundle_totals', 'hot_updater_v1_native_builds', 'hot_updater_v1_bundle_events', 'hot_updater_v1_bundle_events__byBundle', 'hot_updater_v1_bundle_event_heads', 'hot_updater_v1_insights_overview', 'hot_updater_v1_insights_sketches', 'hot_updater_v1_insights_overview_daily', 'hot_updater_v1_insights_sketches_daily', 'hot_updater_v1_insights_overview_lifetime', 'hot_updater_v1_insights_sketches_lifetime', 'hot_updater_v1_insights_distribution', 'hot_updater_v1_insights_builtin_distribution', 'hot_updater_v1_insights_latest_by_bundle', 'hot_updater_v1_insights_outcomes', 'hot_updater_v1_insights_failures', 'hot_updater_v1_api_keys', 'hot_updater_v1_remote_config_active', 'hot_updater_v1_remote_config_versions', 'hot_updater_v1_private_hot_updater_settings', 'hot_updater_v1__hu_write']) THEN
35
+ RAISE EXCEPTION 'hot_updater_v1_apply: % is not a Hot Updater table', target USING ERRCODE = '42501';
36
+ END IF;
37
+ END LOOP;
38
+ shape := regexp_replace(regexp_replace(statement, '^INSERT INTO "[^"]+" \(', 'INSERT INTO ('), '"[^"]*"', '"', 'g');
39
+ IF shape !~ '^(SELECT|INSERT INTO|UPDATE|DELETE FROM) '
40
+ OR shape ~ '"[[:space:]]*\('
41
+ OR shape ~ '[;''\\]|--|/\*'
42
+ OR regexp_replace(shape, '\m(SELECT|FROM|WHERE|AND|OR|NOT|EXISTS|INSERT|INTO|VALUES|UPDATE|SET|DELETE|ORDER|BY|ASC|DESC|LIMIT|IS|NULL|ON|CONFLICT|DO|JOIN|FOR|bigint|double|precision|boolean|jsonb|i|b)\M', '', 'gi') !~ '^[[:space:]"(),.*=<>+0-9$:-]*$' THEN
43
+ RAISE EXCEPTION 'hot_updater_v1_apply: statement not allowed' USING ERRCODE = '42501';
44
+ END IF;
45
+ IF statement ~ '^SELECT ' THEN
46
+ rows := '[]'::jsonb;
47
+ FOR found IN EXECUTE statement USING item->'params' LOOP
48
+ rows := rows || jsonb_build_array(to_jsonb(found));
49
+ END LOOP;
50
+ results := results || jsonb_build_array(jsonb_build_object('rows', rows, 'changes', 0));
51
+ ELSE
52
+ EXECUTE statement USING item->'params';
53
+ GET DIAGNOSTICS changed = ROW_COUNT;
54
+ results := results || jsonb_build_array(jsonb_build_object('rows', '[]'::jsonb, 'changes', changed));
55
+ END IF;
56
+ END LOOP;
57
+ RETURN results;
58
+ END;
59
+ $apply$;
60
+
61
+ REVOKE EXECUTE ON FUNCTION public.hot_updater_v1_apply(jsonb) FROM PUBLIC, anon, authenticated;
62
+
63
+ GRANT EXECUTE ON FUNCTION public.hot_updater_v1_apply(jsonb) TO service_role;
64
+
65
+ INSERT INTO "hot_updater_v1_private_hot_updater_settings" ("key", "value", "_v") VALUES ('schema.engine', '1', 0) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value";
66
+
67
+ INSERT INTO "hot_updater_v1_private_hot_updater_settings" ("key", "value", "_v") VALUES ('schema.core', '1.2.0', 0) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value";
68
+
69
+ INSERT INTO "hot_updater_v1_private_hot_updater_settings" ("key", "value", "_v") VALUES ('schema.insights', '1.0.0', 0) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value";
70
+
71
+ INSERT INTO "hot_updater_v1_private_hot_updater_settings" ("key", "value", "_v") VALUES ('schema.apiKeys', '1.0.0', 0) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value";
72
+
73
+ INSERT INTO "hot_updater_v1_private_hot_updater_settings" ("key", "value", "_v") VALUES ('schema.remoteConfig', '1.0.0', 0) ON CONFLICT ("key") DO UPDATE SET "value" = excluded."value";