@hot-updater/supabase 1.0.0-rc.2 → 1.0.0-rc.21

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.
@@ -0,0 +1,209 @@
1
+ import { SETTINGS_TABLE, createEngineDatabase, createSqlAdapter, createStorageAdapter, createStorageKeyBuilder, createStorageUri, parseStorageUri } from "@hot-updater/plugin-core";
2
+ import { createClient } from "@supabase/supabase-js";
3
+ import { apiKeys } from "@hot-updater/server/plugins/api-keys";
4
+ import { insights } from "@hot-updater/server/plugins/insights";
5
+ //#region src/supabaseConfig.ts
6
+ const resolveSupabaseServiceRoleKey = (config) => {
7
+ if (!config.supabaseServiceRoleKey) throw new Error("Supabase service role key is required. Set supabaseServiceRoleKey.");
8
+ return config.supabaseServiceRoleKey;
9
+ };
10
+ //#endregion
11
+ //#region src/supabaseInfrastructureNames.ts
12
+ /** Namespaces Hot Updater's tables beside a project's own, and beside v0's. */
13
+ const SUPABASE_TABLE_PREFIX = "hot_updater_v1_";
14
+ const SUPABASE_APPLY_FUNCTION = "hot_updater_v1_apply";
15
+ "" + SETTINGS_TABLE.name;
16
+ //#endregion
17
+ //#region src/supabaseExecutor.ts
18
+ /**
19
+ * The apply RPC binds one jsonb array, so each placeholder becomes a typed
20
+ * read of it, `($1->>k)::bigint`, and a null becomes `NULL`. No value is
21
+ * ever written into the SQL.
22
+ */
23
+ const toApplyStatement = ({ sql, params }) => {
24
+ const values = [];
25
+ return {
26
+ sql: sql.replace(/\$(\d+)/gu, (_, position) => {
27
+ const value = params[Number(position) - 1];
28
+ if (value === null || value === void 0) return "NULL";
29
+ values.push(value);
30
+ const read = `($1->>${values.length - 1})`;
31
+ if (typeof value === "boolean") return `${read}::boolean`;
32
+ if (typeof value === "number") return `${read}::${Number.isInteger(value) ? "bigint" : "double precision"}`;
33
+ return read;
34
+ }),
35
+ params: values
36
+ };
37
+ };
38
+ /** PostgREST's code for a missing function or table reads as a missing table. */
39
+ const withCode = (error) => Object.assign(new Error(error.message, { cause: error }), { code: error.code === "PGRST202" || error.code === "PGRST205" ? "42P01" : error.code });
40
+ /**
41
+ * The SQL core's executor over Supabase: every statement goes through the
42
+ * apply RPC, and a write is one RPC call, so one transaction.
43
+ */
44
+ const supabaseExecutor = (client) => {
45
+ const apply = async (statements) => {
46
+ const { data, error } = await client.rpc(SUPABASE_APPLY_FUNCTION, { p_statements: statements.map(toApplyStatement) });
47
+ if (error) throw withCode(error);
48
+ return data;
49
+ };
50
+ return {
51
+ dialect: "postgresql",
52
+ execute: async (statement) => (await apply([statement]))[0],
53
+ batch: apply,
54
+ transaction: () => {
55
+ throw new Error("Supabase writes are batches through the apply RPC.");
56
+ }
57
+ };
58
+ };
59
+ //#endregion
60
+ //#region src/supabaseDatabase.ts
61
+ /**
62
+ * Hot Updater's database on Supabase: the storage engine through the shared
63
+ * SQL core, fenced by the schema settings. Every read and write goes through
64
+ * the service-role-only apply RPC, which runs no DDL, so Supabase migrations
65
+ * create the tables.
66
+ */
67
+ const supabaseDatabase = (config) => {
68
+ const { migrations: _migrations, ...adapter } = createSqlAdapter({
69
+ executor: supabaseExecutor(createClient(config.supabaseUrl, resolveSupabaseServiceRoleKey(config))),
70
+ tablePrefix: SUPABASE_TABLE_PREFIX
71
+ });
72
+ return createEngineDatabase({
73
+ name: "supabaseDatabase",
74
+ adapter
75
+ });
76
+ };
77
+ //#endregion
78
+ //#region src/supabaseSignedUrlBatcher.ts
79
+ function getErrorMessage(error) {
80
+ if (error instanceof Error) return error.message;
81
+ if (error && typeof error === "object" && "message" in error && typeof error.message === "string") return error.message;
82
+ return String(error);
83
+ }
84
+ const createSupabaseSignedUrlBatcher = ({ createSignedUrls, expiresIn, formatObjectPath }) => {
85
+ let pendingByBucket = /* @__PURE__ */ new Map();
86
+ let flushScheduled = false;
87
+ const createSignedUrlError = (bucketName, key, error) => /* @__PURE__ */ new Error(`Failed to generate download URL for "${formatObjectPath(bucketName, key)}": ${getErrorMessage(error)}`);
88
+ const flush = async () => {
89
+ const batches = pendingByBucket;
90
+ pendingByBucket = /* @__PURE__ */ new Map();
91
+ flushScheduled = false;
92
+ await Promise.all([...batches.entries()].map(async ([bucketName, pending]) => {
93
+ try {
94
+ const { data, error } = await createSignedUrls(bucketName, pending.map(({ key }) => key), expiresIn);
95
+ if (error || !data) {
96
+ const batchError = error ?? /* @__PURE__ */ new Error("missing signed URL response");
97
+ for (const request of pending) request.reject(createSignedUrlError(bucketName, request.key, batchError));
98
+ return;
99
+ }
100
+ pending.forEach((request, index) => {
101
+ const result = data[index];
102
+ if (!result?.error && result?.signedUrl) {
103
+ request.resolve(result.signedUrl);
104
+ return;
105
+ }
106
+ request.reject(createSignedUrlError(bucketName, request.key, result?.error ?? /* @__PURE__ */ new Error("missing signed URL")));
107
+ });
108
+ } catch (error) {
109
+ for (const request of pending) request.reject(createSignedUrlError(bucketName, request.key, error));
110
+ }
111
+ }));
112
+ };
113
+ return (bucketName, key) => new Promise((resolve, reject) => {
114
+ const pending = pendingByBucket.get(bucketName) ?? [];
115
+ pending.push({
116
+ key,
117
+ reject,
118
+ resolve
119
+ });
120
+ pendingByBucket.set(bucketName, pending);
121
+ if (!flushScheduled) {
122
+ flushScheduled = true;
123
+ queueMicrotask(() => {
124
+ flush();
125
+ });
126
+ }
127
+ });
128
+ };
129
+ //#endregion
130
+ //#region src/supabaseStorage.ts
131
+ const isNotFoundError = (error) => error?.message?.toLowerCase().includes("not found") === true;
132
+ const isMissingExistsError = (error) => {
133
+ if (typeof error !== "object" || error === null || Reflect.get(error, "name") !== "StorageUnknownError") return false;
134
+ const originalError = Reflect.get(error, "originalError");
135
+ if (typeof originalError !== "object" || originalError === null) return false;
136
+ const status = Reflect.get(originalError, "status");
137
+ return status === 400 || status === 404;
138
+ };
139
+ const supabaseStorage = (config) => {
140
+ const bucket = createClient(config.supabaseUrl, resolveSupabaseServiceRoleKey(config)).storage.from(config.bucketName);
141
+ const getStorageKey = createStorageKeyBuilder(config.basePath);
142
+ const resolveSignedUrl = createSupabaseSignedUrlBatcher({
143
+ createSignedUrls: (_bucketName, keys, expiresIn) => bucket.createSignedUrls(keys, expiresIn),
144
+ expiresIn: config.signedUrlExpiresIn ?? 3600,
145
+ formatObjectPath: (_bucketName, key) => key
146
+ });
147
+ const parseAndValidate = (storageUri) => {
148
+ const parsed = parseStorageUri(storageUri, "supabase-storage");
149
+ if (parsed.bucket !== config.bucketName) throw new Error(`Bucket name mismatch: expected "${config.bucketName}", but found "${parsed.bucket}".`);
150
+ return parsed;
151
+ };
152
+ return createStorageAdapter({
153
+ name: "supabaseStorage",
154
+ protocol: "supabase-storage",
155
+ async put({ key, body, contentLength, contentType }) {
156
+ const storageKey = getStorageKey(key);
157
+ const { error } = await bucket.upload(storageKey, body, {
158
+ contentType,
159
+ cacheControl: "max-age=31536000",
160
+ upsert: true,
161
+ duplex: "half",
162
+ ...contentLength === void 0 ? {} : { headers: { "content-length": String(contentLength) } }
163
+ });
164
+ if (error) throw error;
165
+ return { storageUri: createStorageUri({
166
+ protocol: "supabase-storage",
167
+ bucket: config.bucketName,
168
+ key: storageKey
169
+ }) };
170
+ },
171
+ async get({ storageUri }) {
172
+ const { key } = parseAndValidate(storageUri);
173
+ const { data, error } = await bucket.download(key);
174
+ if (error) {
175
+ if (isNotFoundError(error)) return { response: null };
176
+ throw new Error(`Failed to download storage object: ${error.message}`);
177
+ }
178
+ return { response: data ? new Response(data) : null };
179
+ },
180
+ async getDownloadUrl({ storageUri }) {
181
+ const { key } = parseAndValidate(storageUri);
182
+ return { url: await resolveSignedUrl(config.bucketName, key) };
183
+ },
184
+ async exists({ storageUri }) {
185
+ const { key } = parseAndValidate(storageUri);
186
+ const { data, error } = await bucket.exists(key);
187
+ if (error) {
188
+ if (isMissingExistsError(error)) return { exists: false };
189
+ throw error;
190
+ }
191
+ return { exists: data };
192
+ },
193
+ async delete({ storageUri }) {
194
+ const { key } = parseAndValidate(storageUri);
195
+ const { error } = await bucket.remove([key]);
196
+ if (error && !isNotFoundError(error)) throw new Error(`Failed to delete storage object: ${error.message}`);
197
+ return { deleted: true };
198
+ }
199
+ });
200
+ };
201
+ //#endregion
202
+ //#region src/plugins.ts
203
+ /**
204
+ * The plugins the managed Supabase server runs: Insights, and API keys on every
205
+ * client route. Its Edge Function imports this list.
206
+ */
207
+ const plugins = [insights(), apiKeys()];
208
+ //#endregion
209
+ export { SUPABASE_TABLE_PREFIX as a, SUPABASE_APPLY_FUNCTION as i, supabaseStorage as n, supabaseDatabase as r, plugins as t };
@@ -0,0 +1,238 @@
1
+ let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
2
+ let _supabase_supabase_js = require("@supabase/supabase-js");
3
+ let _hot_updater_server_plugins_api_keys = require("@hot-updater/server/plugins/api-keys");
4
+ let _hot_updater_server_plugins_insights = require("@hot-updater/server/plugins/insights");
5
+ //#region src/supabaseConfig.ts
6
+ const resolveSupabaseServiceRoleKey = (config) => {
7
+ if (!config.supabaseServiceRoleKey) throw new Error("Supabase service role key is required. Set supabaseServiceRoleKey.");
8
+ return config.supabaseServiceRoleKey;
9
+ };
10
+ //#endregion
11
+ //#region src/supabaseInfrastructureNames.ts
12
+ /** Namespaces Hot Updater's tables beside a project's own, and beside v0's. */
13
+ const SUPABASE_TABLE_PREFIX = "hot_updater_v1_";
14
+ const SUPABASE_APPLY_FUNCTION = "hot_updater_v1_apply";
15
+ "" + _hot_updater_plugin_core.SETTINGS_TABLE.name;
16
+ //#endregion
17
+ //#region src/supabaseExecutor.ts
18
+ /**
19
+ * The apply RPC binds one jsonb array, so each placeholder becomes a typed
20
+ * read of it, `($1->>k)::bigint`, and a null becomes `NULL`. No value is
21
+ * ever written into the SQL.
22
+ */
23
+ const toApplyStatement = ({ sql, params }) => {
24
+ const values = [];
25
+ return {
26
+ sql: sql.replace(/\$(\d+)/gu, (_, position) => {
27
+ const value = params[Number(position) - 1];
28
+ if (value === null || value === void 0) return "NULL";
29
+ values.push(value);
30
+ const read = `($1->>${values.length - 1})`;
31
+ if (typeof value === "boolean") return `${read}::boolean`;
32
+ if (typeof value === "number") return `${read}::${Number.isInteger(value) ? "bigint" : "double precision"}`;
33
+ return read;
34
+ }),
35
+ params: values
36
+ };
37
+ };
38
+ /** PostgREST's code for a missing function or table reads as a missing table. */
39
+ const withCode = (error) => Object.assign(new Error(error.message, { cause: error }), { code: error.code === "PGRST202" || error.code === "PGRST205" ? "42P01" : error.code });
40
+ /**
41
+ * The SQL core's executor over Supabase: every statement goes through the
42
+ * apply RPC, and a write is one RPC call, so one transaction.
43
+ */
44
+ const supabaseExecutor = (client) => {
45
+ const apply = async (statements) => {
46
+ const { data, error } = await client.rpc(SUPABASE_APPLY_FUNCTION, { p_statements: statements.map(toApplyStatement) });
47
+ if (error) throw withCode(error);
48
+ return data;
49
+ };
50
+ return {
51
+ dialect: "postgresql",
52
+ execute: async (statement) => (await apply([statement]))[0],
53
+ batch: apply,
54
+ transaction: () => {
55
+ throw new Error("Supabase writes are batches through the apply RPC.");
56
+ }
57
+ };
58
+ };
59
+ //#endregion
60
+ //#region src/supabaseDatabase.ts
61
+ /**
62
+ * Hot Updater's database on Supabase: the storage engine through the shared
63
+ * SQL core, fenced by the schema settings. Every read and write goes through
64
+ * the service-role-only apply RPC, which runs no DDL, so Supabase migrations
65
+ * create the tables.
66
+ */
67
+ const supabaseDatabase = (config) => {
68
+ const { migrations: _migrations, ...adapter } = (0, _hot_updater_plugin_core.createSqlAdapter)({
69
+ executor: supabaseExecutor((0, _supabase_supabase_js.createClient)(config.supabaseUrl, resolveSupabaseServiceRoleKey(config))),
70
+ tablePrefix: SUPABASE_TABLE_PREFIX
71
+ });
72
+ return (0, _hot_updater_plugin_core.createEngineDatabase)({
73
+ name: "supabaseDatabase",
74
+ adapter
75
+ });
76
+ };
77
+ //#endregion
78
+ //#region src/supabaseSignedUrlBatcher.ts
79
+ function getErrorMessage(error) {
80
+ if (error instanceof Error) return error.message;
81
+ if (error && typeof error === "object" && "message" in error && typeof error.message === "string") return error.message;
82
+ return String(error);
83
+ }
84
+ const createSupabaseSignedUrlBatcher = ({ createSignedUrls, expiresIn, formatObjectPath }) => {
85
+ let pendingByBucket = /* @__PURE__ */ new Map();
86
+ let flushScheduled = false;
87
+ const createSignedUrlError = (bucketName, key, error) => /* @__PURE__ */ new Error(`Failed to generate download URL for "${formatObjectPath(bucketName, key)}": ${getErrorMessage(error)}`);
88
+ const flush = async () => {
89
+ const batches = pendingByBucket;
90
+ pendingByBucket = /* @__PURE__ */ new Map();
91
+ flushScheduled = false;
92
+ await Promise.all([...batches.entries()].map(async ([bucketName, pending]) => {
93
+ try {
94
+ const { data, error } = await createSignedUrls(bucketName, pending.map(({ key }) => key), expiresIn);
95
+ if (error || !data) {
96
+ const batchError = error ?? /* @__PURE__ */ new Error("missing signed URL response");
97
+ for (const request of pending) request.reject(createSignedUrlError(bucketName, request.key, batchError));
98
+ return;
99
+ }
100
+ pending.forEach((request, index) => {
101
+ const result = data[index];
102
+ if (!result?.error && result?.signedUrl) {
103
+ request.resolve(result.signedUrl);
104
+ return;
105
+ }
106
+ request.reject(createSignedUrlError(bucketName, request.key, result?.error ?? /* @__PURE__ */ new Error("missing signed URL")));
107
+ });
108
+ } catch (error) {
109
+ for (const request of pending) request.reject(createSignedUrlError(bucketName, request.key, error));
110
+ }
111
+ }));
112
+ };
113
+ return (bucketName, key) => new Promise((resolve, reject) => {
114
+ const pending = pendingByBucket.get(bucketName) ?? [];
115
+ pending.push({
116
+ key,
117
+ reject,
118
+ resolve
119
+ });
120
+ pendingByBucket.set(bucketName, pending);
121
+ if (!flushScheduled) {
122
+ flushScheduled = true;
123
+ queueMicrotask(() => {
124
+ flush();
125
+ });
126
+ }
127
+ });
128
+ };
129
+ //#endregion
130
+ //#region src/supabaseStorage.ts
131
+ const isNotFoundError = (error) => error?.message?.toLowerCase().includes("not found") === true;
132
+ const isMissingExistsError = (error) => {
133
+ if (typeof error !== "object" || error === null || Reflect.get(error, "name") !== "StorageUnknownError") return false;
134
+ const originalError = Reflect.get(error, "originalError");
135
+ if (typeof originalError !== "object" || originalError === null) return false;
136
+ const status = Reflect.get(originalError, "status");
137
+ return status === 400 || status === 404;
138
+ };
139
+ const supabaseStorage = (config) => {
140
+ const bucket = (0, _supabase_supabase_js.createClient)(config.supabaseUrl, resolveSupabaseServiceRoleKey(config)).storage.from(config.bucketName);
141
+ const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(config.basePath);
142
+ const resolveSignedUrl = createSupabaseSignedUrlBatcher({
143
+ createSignedUrls: (_bucketName, keys, expiresIn) => bucket.createSignedUrls(keys, expiresIn),
144
+ expiresIn: config.signedUrlExpiresIn ?? 3600,
145
+ formatObjectPath: (_bucketName, key) => key
146
+ });
147
+ const parseAndValidate = (storageUri) => {
148
+ const parsed = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "supabase-storage");
149
+ if (parsed.bucket !== config.bucketName) throw new Error(`Bucket name mismatch: expected "${config.bucketName}", but found "${parsed.bucket}".`);
150
+ return parsed;
151
+ };
152
+ return (0, _hot_updater_plugin_core.createStorageAdapter)({
153
+ name: "supabaseStorage",
154
+ protocol: "supabase-storage",
155
+ async put({ key, body, contentLength, contentType }) {
156
+ const storageKey = getStorageKey(key);
157
+ const { error } = await bucket.upload(storageKey, body, {
158
+ contentType,
159
+ cacheControl: "max-age=31536000",
160
+ upsert: true,
161
+ duplex: "half",
162
+ ...contentLength === void 0 ? {} : { headers: { "content-length": String(contentLength) } }
163
+ });
164
+ if (error) throw error;
165
+ return { storageUri: (0, _hot_updater_plugin_core.createStorageUri)({
166
+ protocol: "supabase-storage",
167
+ bucket: config.bucketName,
168
+ key: storageKey
169
+ }) };
170
+ },
171
+ async get({ storageUri }) {
172
+ const { key } = parseAndValidate(storageUri);
173
+ const { data, error } = await bucket.download(key);
174
+ if (error) {
175
+ if (isNotFoundError(error)) return { response: null };
176
+ throw new Error(`Failed to download storage object: ${error.message}`);
177
+ }
178
+ return { response: data ? new Response(data) : null };
179
+ },
180
+ async getDownloadUrl({ storageUri }) {
181
+ const { key } = parseAndValidate(storageUri);
182
+ return { url: await resolveSignedUrl(config.bucketName, key) };
183
+ },
184
+ async exists({ storageUri }) {
185
+ const { key } = parseAndValidate(storageUri);
186
+ const { data, error } = await bucket.exists(key);
187
+ if (error) {
188
+ if (isMissingExistsError(error)) return { exists: false };
189
+ throw error;
190
+ }
191
+ return { exists: data };
192
+ },
193
+ async delete({ storageUri }) {
194
+ const { key } = parseAndValidate(storageUri);
195
+ const { error } = await bucket.remove([key]);
196
+ if (error && !isNotFoundError(error)) throw new Error(`Failed to delete storage object: ${error.message}`);
197
+ return { deleted: true };
198
+ }
199
+ });
200
+ };
201
+ //#endregion
202
+ //#region src/plugins.ts
203
+ /**
204
+ * The plugins the managed Supabase server runs: Insights, and API keys on every
205
+ * client route. Its Edge Function imports this list.
206
+ */
207
+ const plugins = [(0, _hot_updater_server_plugins_insights.insights)(), (0, _hot_updater_server_plugins_api_keys.apiKeys)()];
208
+ //#endregion
209
+ Object.defineProperty(exports, "SUPABASE_APPLY_FUNCTION", {
210
+ enumerable: true,
211
+ get: function() {
212
+ return SUPABASE_APPLY_FUNCTION;
213
+ }
214
+ });
215
+ Object.defineProperty(exports, "SUPABASE_TABLE_PREFIX", {
216
+ enumerable: true,
217
+ get: function() {
218
+ return SUPABASE_TABLE_PREFIX;
219
+ }
220
+ });
221
+ Object.defineProperty(exports, "plugins", {
222
+ enumerable: true,
223
+ get: function() {
224
+ return plugins;
225
+ }
226
+ });
227
+ Object.defineProperty(exports, "supabaseDatabase", {
228
+ enumerable: true,
229
+ get: function() {
230
+ return supabaseDatabase;
231
+ }
232
+ });
233
+ Object.defineProperty(exports, "supabaseStorage", {
234
+ enumerable: true,
235
+ get: function() {
236
+ return supabaseStorage;
237
+ }
238
+ });