@edge-base/server 0.2.1 → 0.2.3
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/admin-build/_app/immutable/chunks/{DjOEv9M9.js → A_3UuvCe.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Dqk2TGNU.js → B-_-hJ9o.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BFs_qStz.js → B5Nwfelm.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B0QyxC2M.js → BxoNtYHK.js} +3 -3
- package/admin-build/_app/immutable/chunks/{BsFiK_FJ.js → CZ0TVkCa.js} +1 -1
- package/admin-build/_app/immutable/chunks/{k0CIJkw4.js → CzSAxmuj.js} +1 -1
- package/admin-build/_app/immutable/chunks/{D-x55wdW.js → DCKcAiQH.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CSGrwS7E.js → DCvwWZrm.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BTJcQFEp.js → DRqPU3wD.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CqUxCvs_.js → Dc1-6Po6.js} +1 -1
- package/admin-build/_app/immutable/chunks/{D755Tqat.js → DiyBpamp.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BcIUK2sk.js → Dlty5069.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BY07qVPA.js → DpVAayDG.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BCKr7yKd.js → Du5vWVa2.js} +1 -1
- package/admin-build/_app/immutable/chunks/{m9QZTyVV.js → byv2rTy8.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DnLqc9L1.js → nZvorU8i.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.BTsq3_xq.js → app.CfrmEXPD.js} +2 -2
- package/admin-build/_app/immutable/entry/start.l1WvHznQ.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.BZ00WDYH.js → 0.Cn2BZ4da.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.RzSJ3yyr.js → 1.Dv4LX_Co.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.D-rsiquF.js → 10.DPVv3kat.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.l7-bgtFD.js → 11.CiCb6Ayu.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.Dkq0H7B5.js → 12.CIPyeekF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.DtK_4oRz.js → 13.Z15Lt36e.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.BKo7-AMx.js → 14.s0l5bAq3.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.CQAj_6lq.js → 15.UwSSNO76.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.XVIG-Ffr.js → 16.qiD8i883.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.g6raZLCM.js → 17.Dy3dcSvu.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.IQz6a3T6.js → 18.DeXyPYsO.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.CAAZ8i8h.js → 19.CAbuyS6w.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.BPcX3KPj.js → 20.Bec0T7un.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.DuDYelMY.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.Br5AG_5Z.js → 22.CdVprrv2.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.KjbrdXoE.js → 23.Y8RzVLoF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.C3n2-hgw.js → 24.CWhHYFBx.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.SFDSBzHd.js → 25.wCBplOVt.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.D95vui6E.js → 26.Cod_JRFK.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.FgLgdjwB.js → 27.BO2HVMu9.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.B9sYYm1F.js → 28.DxG-FBVQ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.DyqZ_wbN.js → 29.CjGqWGvE.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.Bzo2yVIO.js → 3.By3_OmdZ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.c1CiNwiS.js → 30.M_H7Htpq.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.CXty66Vh.js → 31.DEU18izM.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.BgQaXZ27.js → 4.DeYhKtzJ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.BuJrHvxH.js → 5.9WLgxhrD.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.CkBBC94k.js → 6.BdT2i_dd.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.D2YBvNFM.js → 7.CHq0s4K6.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.D8qQWo_z.js → 8.DuvRw-XZ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.BLDLX5hV.js → 9.C2Ub82wn.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/package.json +3 -2
- package/src/__tests__/d1-live-broadcast-verification.test.ts +271 -0
- package/src/__tests__/database-live-do.test.ts +50 -0
- package/src/__tests__/database-live-emitter.test.ts +116 -1
- package/src/__tests__/error-format.test.ts +63 -0
- package/src/__tests__/functions-context.test.ts +592 -35
- package/src/__tests__/meta-export-coverage.test.ts +1 -0
- package/src/__tests__/postgres-field-ops-compat.test.ts +110 -0
- package/src/__tests__/provider-aware-sql.test.ts +157 -0
- package/src/__tests__/room-auth-state-loss.test.ts +124 -0
- package/src/__tests__/runtime-surface-accounting.test.ts +0 -4
- package/src/__tests__/sql-route.test.ts +187 -76
- package/src/durable-objects/database-live-do.ts +46 -1
- package/src/durable-objects/room-runtime-base.ts +26 -2
- package/src/durable-objects/rooms-do.ts +1 -1
- package/src/lib/admin-db-target.ts +30 -74
- package/src/lib/d1-handler.ts +45 -14
- package/src/lib/database-live-emitter.ts +57 -16
- package/src/lib/functions.ts +332 -454
- package/src/lib/internal-transport.ts +316 -0
- package/src/lib/plugin-migrations.ts +39 -39
- package/src/lib/postgres-handler.ts +39 -11
- package/src/lib/provider-aware-sql.ts +827 -0
- package/src/routes/admin.ts +7 -1
- package/src/routes/auth.ts +11 -12
- package/src/routes/sql.ts +51 -76
- package/src/routes/storage.ts +11 -12
- package/src/types.ts +2 -0
- package/admin-build/_app/immutable/entry/start.zXCirpgY.js +0 -1
- package/admin-build/_app/immutable/nodes/21.DoPabrY_.js +0 -1
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal HttpTransport implementation for server-side function context.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the old TableProxy by implementing the same HttpTransport interface
|
|
5
|
+
* that @edge-base/core's DefaultDbApi expects. This lets us use the real
|
|
6
|
+
* TableRef/DbRef classes from the core SDK while routing requests directly
|
|
7
|
+
* to D1, PostgreSQL, or DurableObject handlers — no HTTP round-trip.
|
|
8
|
+
*/
|
|
9
|
+
import type { HttpTransport } from '@edge-base/core';
|
|
10
|
+
import type { EdgeBaseConfig } from '@edge-base/shared';
|
|
11
|
+
import { getDbDoName, callDO, shouldRouteToD1 } from './do-router.js';
|
|
12
|
+
import { handleD1Request } from './d1-handler.js';
|
|
13
|
+
import { handlePgRequest } from './postgres-handler.js';
|
|
14
|
+
import { buildInternalHandlerContext } from './internal-request.js';
|
|
15
|
+
import type { Env } from '../types.js';
|
|
16
|
+
|
|
17
|
+
export interface InternalTransportOptions {
|
|
18
|
+
databaseNamespace: DurableObjectNamespace;
|
|
19
|
+
config: EdgeBaseConfig;
|
|
20
|
+
workerUrl?: string;
|
|
21
|
+
serviceKey?: string;
|
|
22
|
+
env?: Env;
|
|
23
|
+
executionCtx?: ExecutionContext;
|
|
24
|
+
preferDirectDo?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* When set, the transport knows this DbRef targets a specific
|
|
27
|
+
* namespace + optional instanceId. This avoids ambiguous path parsing
|
|
28
|
+
* when instanceId happens to be "tables".
|
|
29
|
+
*/
|
|
30
|
+
dbContext?: { namespace: string; instanceId?: string };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a DefaultDbApi path into routing components.
|
|
35
|
+
*
|
|
36
|
+
* Paths follow two patterns:
|
|
37
|
+
* /api/db/{namespace}/tables/{table}[/rest] → static DB
|
|
38
|
+
* /api/db/{namespace}/{instanceId}/tables/{table}[/rest] → dynamic DB
|
|
39
|
+
*
|
|
40
|
+
* Returns namespace, optional instanceId, tableName, and directPath
|
|
41
|
+
* (everything from /tables/... onward, which D1/PG handlers expect).
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* Parse a DefaultDbApi path, optionally guided by known dbContext.
|
|
45
|
+
*
|
|
46
|
+
* When dbContext is provided (recommended), we know whether this is a
|
|
47
|
+
* static or dynamic DB, so we can unambiguously find the 'tables' keyword
|
|
48
|
+
* even when instanceId === 'tables'.
|
|
49
|
+
*
|
|
50
|
+
* Without dbContext, falls back to heuristic: first 'tables' at index 1
|
|
51
|
+
* means static, at index 2 means dynamic.
|
|
52
|
+
*/
|
|
53
|
+
function parsePath(
|
|
54
|
+
path: string,
|
|
55
|
+
dbContext?: { namespace: string; instanceId?: string },
|
|
56
|
+
): {
|
|
57
|
+
namespace: string;
|
|
58
|
+
instanceId?: string;
|
|
59
|
+
tableName: string;
|
|
60
|
+
directPath: string;
|
|
61
|
+
} {
|
|
62
|
+
// Strip leading /api/db/
|
|
63
|
+
const stripped = path.replace(/^\/api\/db\//, '');
|
|
64
|
+
const segments = stripped.split('/');
|
|
65
|
+
|
|
66
|
+
let tablesIdx: number;
|
|
67
|
+
if (dbContext) {
|
|
68
|
+
// We know the shape: static has 'tables' at index 1, dynamic at index 2
|
|
69
|
+
tablesIdx = dbContext.instanceId ? 2 : 1;
|
|
70
|
+
} else {
|
|
71
|
+
// Heuristic fallback: find first 'tables' keyword
|
|
72
|
+
tablesIdx = segments.indexOf('tables', 1);
|
|
73
|
+
}
|
|
74
|
+
if (tablesIdx < 0 || segments[tablesIdx] !== 'tables') {
|
|
75
|
+
throw new Error(`Invalid DB path: missing 'tables' segment in ${path}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const namespace = segments[0];
|
|
79
|
+
const instanceId = tablesIdx === 2 ? segments[1] : undefined;
|
|
80
|
+
const rawTableName = segments[tablesIdx + 1];
|
|
81
|
+
// Decode URL-encoded table names (e.g. 'plugin-a%2Fevents' → 'plugin-a/events')
|
|
82
|
+
// Use decoded name consistently in both tableName and directPath so that
|
|
83
|
+
// handler suffix parsing (doPath.replace(`/tables/${tableName}`, '')) matches.
|
|
84
|
+
const tableName = decodeURIComponent(rawTableName);
|
|
85
|
+
const rest = segments.slice(tablesIdx + 2);
|
|
86
|
+
const directPath = `/tables/${tableName}${rest.length ? '/' + rest.join('/') : ''}`;
|
|
87
|
+
|
|
88
|
+
return { namespace, instanceId, tableName, directPath };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class InternalHttpTransport implements HttpTransport {
|
|
92
|
+
private readonly databaseNamespace: DurableObjectNamespace;
|
|
93
|
+
private readonly config: EdgeBaseConfig;
|
|
94
|
+
private readonly workerUrl?: string;
|
|
95
|
+
private readonly serviceKey?: string;
|
|
96
|
+
private readonly env?: Env;
|
|
97
|
+
private readonly executionCtx?: ExecutionContext;
|
|
98
|
+
private readonly preferDirectDo: boolean;
|
|
99
|
+
private readonly dbContext?: { namespace: string; instanceId?: string };
|
|
100
|
+
|
|
101
|
+
constructor(options: InternalTransportOptions) {
|
|
102
|
+
this.databaseNamespace = options.databaseNamespace;
|
|
103
|
+
this.config = options.config;
|
|
104
|
+
this.workerUrl = options.workerUrl;
|
|
105
|
+
this.serviceKey = options.serviceKey;
|
|
106
|
+
this.env = options.env;
|
|
107
|
+
this.executionCtx = options.executionCtx;
|
|
108
|
+
this.preferDirectDo = options.preferDirectDo ?? false;
|
|
109
|
+
this.dbContext = options.dbContext;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async request<T>(
|
|
113
|
+
method: string,
|
|
114
|
+
path: string,
|
|
115
|
+
options?: { query?: Record<string, string>; body?: unknown },
|
|
116
|
+
): Promise<T> {
|
|
117
|
+
const { namespace, instanceId, tableName, directPath } = parsePath(path, this.dbContext);
|
|
118
|
+
const doName = getDbDoName(namespace, instanceId);
|
|
119
|
+
|
|
120
|
+
// Build internal headers
|
|
121
|
+
const headers: Record<string, string> = {
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
'X-DO-Name': doName,
|
|
124
|
+
'X-EdgeBase-Internal': 'true',
|
|
125
|
+
};
|
|
126
|
+
if (this.serviceKey) {
|
|
127
|
+
headers['X-EdgeBase-Service-Key'] = this.serviceKey;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Convert query Record to URLSearchParams
|
|
131
|
+
const query = new URLSearchParams();
|
|
132
|
+
if (options?.query) {
|
|
133
|
+
for (const [k, v] of Object.entries(options.query)) {
|
|
134
|
+
if (v !== undefined && v !== '') query.set(k, v);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const body = options?.body as Record<string, unknown> | undefined;
|
|
139
|
+
|
|
140
|
+
// Route to the appropriate handler
|
|
141
|
+
const res = await this.routeRequest(
|
|
142
|
+
method as 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT',
|
|
143
|
+
namespace,
|
|
144
|
+
instanceId,
|
|
145
|
+
tableName,
|
|
146
|
+
directPath,
|
|
147
|
+
doName,
|
|
148
|
+
headers,
|
|
149
|
+
query,
|
|
150
|
+
body,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (!res.ok) {
|
|
154
|
+
const err = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
155
|
+
throw new Error(String(err.message || `Internal request failed: ${res.status}`));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (await res.json()) as T;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async head(_path: string): Promise<boolean> {
|
|
162
|
+
// HEAD is only used by StorageClient.checkFileExists — not relevant for DB ops
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private async routeRequest(
|
|
167
|
+
method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT',
|
|
168
|
+
namespace: string,
|
|
169
|
+
instanceId: string | undefined,
|
|
170
|
+
tableName: string,
|
|
171
|
+
directPath: string,
|
|
172
|
+
doName: string,
|
|
173
|
+
headers: Record<string, string>,
|
|
174
|
+
query: URLSearchParams,
|
|
175
|
+
body?: Record<string, unknown>,
|
|
176
|
+
): Promise<Response> {
|
|
177
|
+
const queryString = Array.from(query.keys()).length > 0 ? `?${query.toString()}` : '';
|
|
178
|
+
const directPathWithQuery = `${directPath}${queryString}`;
|
|
179
|
+
const provider = this.config.databases?.[namespace]?.provider;
|
|
180
|
+
const httpMethod = method === 'PUT' ? 'PATCH' : method; // normalize PUT → PATCH
|
|
181
|
+
|
|
182
|
+
// 1. D1 route
|
|
183
|
+
if (!this.preferDirectDo && shouldRouteToD1(namespace, this.config) && this.env) {
|
|
184
|
+
return this.requestViaD1Handler(httpMethod, namespace, instanceId, tableName, directPath, headers, query, body);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 2. PostgreSQL route
|
|
188
|
+
if ((provider === 'neon' || provider === 'postgres') && this.env) {
|
|
189
|
+
return this.requestViaPgHandler(httpMethod, namespace, instanceId, tableName, directPath, headers, query, body);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 3. Direct DO route
|
|
193
|
+
if (this.env) {
|
|
194
|
+
return this.requestViaDirectDo(httpMethod, doName, directPathWithQuery, headers, body, !!instanceId);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 4. Worker HTTP fallback
|
|
198
|
+
if (this.workerUrl) {
|
|
199
|
+
const apiPath = instanceId
|
|
200
|
+
? `/api/db/${namespace}/${instanceId}${directPathWithQuery}`
|
|
201
|
+
: `/api/db/${namespace}${directPathWithQuery}`;
|
|
202
|
+
return this.requestViaWorker(httpMethod, apiPath, headers, body);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 5. Fallback: direct DO
|
|
206
|
+
return this.requestViaDirectDo(httpMethod, doName, directPathWithQuery, headers, body, !!instanceId);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private async requestViaWorker(
|
|
210
|
+
method: string,
|
|
211
|
+
path: string,
|
|
212
|
+
headers: Record<string, string>,
|
|
213
|
+
body?: Record<string, unknown>,
|
|
214
|
+
): Promise<Response> {
|
|
215
|
+
return fetch(`${this.workerUrl}${path}`, {
|
|
216
|
+
method,
|
|
217
|
+
headers,
|
|
218
|
+
body: body === undefined || method === 'GET' || method === 'DELETE'
|
|
219
|
+
? undefined
|
|
220
|
+
: JSON.stringify(body),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private async requestViaD1Handler(
|
|
225
|
+
method: string,
|
|
226
|
+
namespace: string,
|
|
227
|
+
instanceId: string | undefined,
|
|
228
|
+
tableName: string,
|
|
229
|
+
directPath: string,
|
|
230
|
+
headers: Record<string, string>,
|
|
231
|
+
query: URLSearchParams,
|
|
232
|
+
body?: Record<string, unknown>,
|
|
233
|
+
): Promise<Response> {
|
|
234
|
+
if (!this.env) throw new Error('D1 table proxy requires env.');
|
|
235
|
+
|
|
236
|
+
const queryString = Array.from(query.keys()).length > 0 ? `?${query.toString()}` : '';
|
|
237
|
+
const url = `http://internal/api/db/${namespace}${instanceId ? `/${instanceId}` : ''}${directPath}${queryString}`;
|
|
238
|
+
|
|
239
|
+
const request = new Request(url, {
|
|
240
|
+
method,
|
|
241
|
+
headers,
|
|
242
|
+
body: body === undefined || method === 'GET' || method === 'DELETE'
|
|
243
|
+
? undefined
|
|
244
|
+
: JSON.stringify(body),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return handleD1Request(
|
|
248
|
+
buildInternalHandlerContext({ env: this.env, request, body, executionCtx: this.executionCtx }),
|
|
249
|
+
namespace,
|
|
250
|
+
tableName,
|
|
251
|
+
directPath,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private async requestViaPgHandler(
|
|
256
|
+
method: string,
|
|
257
|
+
namespace: string,
|
|
258
|
+
instanceId: string | undefined,
|
|
259
|
+
tableName: string,
|
|
260
|
+
directPath: string,
|
|
261
|
+
headers: Record<string, string>,
|
|
262
|
+
query: URLSearchParams,
|
|
263
|
+
body?: Record<string, unknown>,
|
|
264
|
+
): Promise<Response> {
|
|
265
|
+
if (!this.env) throw new Error('PostgreSQL table proxy requires env.');
|
|
266
|
+
|
|
267
|
+
const queryString = Array.from(query.keys()).length > 0 ? `?${query.toString()}` : '';
|
|
268
|
+
const url = `http://internal/api/db/${namespace}${instanceId ? `/${instanceId}` : ''}${directPath}${queryString}`;
|
|
269
|
+
|
|
270
|
+
const request = new Request(url, {
|
|
271
|
+
method,
|
|
272
|
+
headers,
|
|
273
|
+
body: body === undefined || method === 'GET' || method === 'DELETE'
|
|
274
|
+
? undefined
|
|
275
|
+
: JSON.stringify(body),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return handlePgRequest(
|
|
279
|
+
buildInternalHandlerContext({ env: this.env, request, body, executionCtx: this.executionCtx }),
|
|
280
|
+
namespace,
|
|
281
|
+
tableName,
|
|
282
|
+
directPath,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private async requestViaDirectDo(
|
|
287
|
+
method: string,
|
|
288
|
+
doName: string,
|
|
289
|
+
directPathWithQuery: string,
|
|
290
|
+
headers: Record<string, string>,
|
|
291
|
+
body?: Record<string, unknown>,
|
|
292
|
+
isDynamic?: boolean,
|
|
293
|
+
): Promise<Response> {
|
|
294
|
+
const res = await callDO(this.databaseNamespace, doName, directPathWithQuery, {
|
|
295
|
+
method,
|
|
296
|
+
body,
|
|
297
|
+
headers,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Handle dynamic DO creation
|
|
301
|
+
if (isDynamic && res.status === 201) {
|
|
302
|
+
const createPayload = await res.clone().json().catch(() => null) as
|
|
303
|
+
| { needsCreate?: boolean }
|
|
304
|
+
| null;
|
|
305
|
+
if (createPayload?.needsCreate) {
|
|
306
|
+
return callDO(this.databaseNamespace, doName, directPathWithQuery, {
|
|
307
|
+
method,
|
|
308
|
+
body,
|
|
309
|
+
headers: { ...headers, 'X-DO-Create-Authorized': '1' },
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return res;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
buildFunctionPushProxy,
|
|
34
34
|
buildAdminAuthContext,
|
|
35
35
|
} from './functions.js';
|
|
36
|
-
import {
|
|
36
|
+
import { executeProviderAwareSql } from './provider-aware-sql.js';
|
|
37
37
|
import { resolveRootServiceKey } from './service-key.js';
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -118,9 +118,9 @@ function arePluginMigrationsCurrentInMemory(plugins: PluginInstance[]): boolean
|
|
|
118
118
|
return false;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
return versionedPlugins.every(
|
|
122
|
-
currentVersionedPlugins.get(plugin.name) === plugin.version
|
|
123
|
-
)
|
|
121
|
+
return versionedPlugins.every(
|
|
122
|
+
(plugin) => currentVersionedPlugins.get(plugin.name) === plugin.version,
|
|
123
|
+
);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
function markPluginsCurrent(plugins: PluginInstance[]): void {
|
|
@@ -217,7 +217,8 @@ async function runMigrationsWithTimeout(
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
function resolvePluginMigrationTimeoutMs(): number {
|
|
220
|
-
const raw =
|
|
220
|
+
const raw =
|
|
221
|
+
typeof process !== 'undefined' ? process.env.EDGEBASE_PLUGIN_MIGRATIONS_TIMEOUT_MS : undefined;
|
|
221
222
|
const parsed = Number(raw);
|
|
222
223
|
if (Number.isFinite(parsed) && parsed > 0) {
|
|
223
224
|
return parsed;
|
|
@@ -250,9 +251,9 @@ async function arePluginMigrationsCurrent(
|
|
|
250
251
|
);
|
|
251
252
|
const versions = new Map(rows.map((row) => [row.key, row.value]));
|
|
252
253
|
|
|
253
|
-
return versionedPlugins.every(
|
|
254
|
-
versions.get(`plugin_version:${plugin.name}`) === plugin.version
|
|
255
|
-
)
|
|
254
|
+
return versionedPlugins.every(
|
|
255
|
+
(plugin) => versions.get(`plugin_version:${plugin.name}`) === plugin.version,
|
|
256
|
+
);
|
|
256
257
|
}
|
|
257
258
|
|
|
258
259
|
// ─── Helpers ───
|
|
@@ -334,6 +335,28 @@ function buildMigrationAdminContext(
|
|
|
334
335
|
return directUrl ?? null;
|
|
335
336
|
}
|
|
336
337
|
|
|
338
|
+
const sqlProviderAware = async (
|
|
339
|
+
namespace: string,
|
|
340
|
+
id: string | undefined,
|
|
341
|
+
query: string,
|
|
342
|
+
params?: unknown[],
|
|
343
|
+
): Promise<unknown[]> => {
|
|
344
|
+
const result = await executeProviderAwareSql(
|
|
345
|
+
{
|
|
346
|
+
env,
|
|
347
|
+
config,
|
|
348
|
+
databaseNamespace: dbNamespace,
|
|
349
|
+
workerUrl,
|
|
350
|
+
serviceKey,
|
|
351
|
+
},
|
|
352
|
+
namespace,
|
|
353
|
+
id,
|
|
354
|
+
query,
|
|
355
|
+
params ?? [],
|
|
356
|
+
);
|
|
357
|
+
return result.rows as unknown[];
|
|
358
|
+
};
|
|
359
|
+
|
|
337
360
|
return {
|
|
338
361
|
db(namespace: string, id?: string) {
|
|
339
362
|
const pgConnStr = resolvePgConnString(namespace);
|
|
@@ -350,40 +373,14 @@ function buildMigrationAdminContext(
|
|
|
350
373
|
return doAdminDb(namespace, id);
|
|
351
374
|
},
|
|
352
375
|
|
|
353
|
-
|
|
376
|
+
sqlProviderAware,
|
|
377
|
+
async sqlWithDirectD1Access(
|
|
354
378
|
namespace: string,
|
|
355
379
|
id: string | undefined,
|
|
356
380
|
query: string,
|
|
357
381
|
params?: unknown[],
|
|
358
382
|
): Promise<unknown[]> {
|
|
359
|
-
|
|
360
|
-
const isDynamicNamespace = !!(dbBlock?.instance || dbBlock?.access?.canCreate || dbBlock?.access?.access);
|
|
361
|
-
if (isDynamicNamespace && !id) {
|
|
362
|
-
throw new Error(`admin.sql() requires an id for dynamic namespace '${namespace}'.`);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const pgConnStr = resolvePgConnString(namespace);
|
|
366
|
-
|
|
367
|
-
// ─── PostgreSQL path ───
|
|
368
|
-
if (pgConnStr) {
|
|
369
|
-
// Ensure schema is initialized before raw SQL
|
|
370
|
-
const dbBlock = config.databases?.[namespace];
|
|
371
|
-
if (dbBlock?.tables) {
|
|
372
|
-
await ensurePgSchema(pgConnStr, namespace, dbBlock.tables);
|
|
373
|
-
}
|
|
374
|
-
const result = await executePostgresQuery(pgConnStr, query, params ?? []);
|
|
375
|
-
return result.rows as unknown[];
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// ─── DO path (existing) ───
|
|
379
|
-
return executeDoSql({
|
|
380
|
-
databaseNamespace: dbNamespace,
|
|
381
|
-
namespace,
|
|
382
|
-
id,
|
|
383
|
-
query,
|
|
384
|
-
params: params ?? [],
|
|
385
|
-
internal: true,
|
|
386
|
-
});
|
|
383
|
+
return sqlProviderAware(namespace, id, query, params);
|
|
387
384
|
},
|
|
388
385
|
|
|
389
386
|
// ─── Convenience shortcut: table(name) → db('shared').table(name) ───
|
|
@@ -523,7 +520,8 @@ function buildPgTableOps(
|
|
|
523
520
|
const setClauses = (updatableCols.length > 0 ? updatableCols : [conflictTarget]).map(
|
|
524
521
|
(col) => `${escapePgIdentifier(col)} = EXCLUDED.${escapePgIdentifier(col)}`,
|
|
525
522
|
);
|
|
526
|
-
const sql =
|
|
523
|
+
const sql =
|
|
524
|
+
`INSERT INTO ${escapePgIdentifier(tableName)} (${cols.map(escapePgIdentifier).join(', ')}) VALUES (${placeholders.join(', ')})` +
|
|
527
525
|
` ON CONFLICT (${escapePgIdentifier(conflictTarget)}) DO UPDATE SET ${setClauses.join(', ')} RETURNING *`;
|
|
528
526
|
const result = await executePostgresQuery(connectionString, sql, vals);
|
|
529
527
|
return result.rows[0]
|
|
@@ -596,7 +594,9 @@ function buildPgTableOps(
|
|
|
596
594
|
}
|
|
597
595
|
|
|
598
596
|
const result = await executePostgresQuery(connectionString, sql, params);
|
|
599
|
-
return {
|
|
597
|
+
return {
|
|
598
|
+
items: result.rows.map((row) => stripInternalPgFields(row as Record<string, unknown>)),
|
|
599
|
+
};
|
|
600
600
|
},
|
|
601
601
|
};
|
|
602
602
|
}
|
|
@@ -298,6 +298,18 @@ function buildHookCtx(
|
|
|
298
298
|
};
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
function scheduleDbLive(
|
|
302
|
+
executionCtx: ExecutionContext,
|
|
303
|
+
promise: Promise<void>,
|
|
304
|
+
context: string,
|
|
305
|
+
): void {
|
|
306
|
+
executionCtx.waitUntil(
|
|
307
|
+
promise.catch((error) => {
|
|
308
|
+
console.warn(`[db-live] ${context} failed`, error);
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
301
313
|
function toFieldErrorData(
|
|
302
314
|
errors: Record<string, string>,
|
|
303
315
|
): Record<string, { code: string; message: string }> {
|
|
@@ -605,7 +617,8 @@ async function handleInsert(
|
|
|
605
617
|
}
|
|
606
618
|
|
|
607
619
|
// Emit database-live event (fire-and-forget)
|
|
608
|
-
|
|
620
|
+
scheduleDbLive(
|
|
621
|
+
c.executionCtx,
|
|
609
622
|
emitDbLiveEvent(
|
|
610
623
|
c.env,
|
|
611
624
|
resolved.namespace,
|
|
@@ -614,6 +627,7 @@ async function handleInsert(
|
|
|
614
627
|
String(inserted.id ?? ''),
|
|
615
628
|
inserted,
|
|
616
629
|
),
|
|
630
|
+
`emit ${isUpsert && isUpdate ? 'modified' : 'added'} ${resolved.namespace}.${tableName}`,
|
|
617
631
|
);
|
|
618
632
|
c.executionCtx.waitUntil(
|
|
619
633
|
executeDbTriggers(
|
|
@@ -733,8 +747,10 @@ async function handleUpdate(
|
|
|
733
747
|
}
|
|
734
748
|
|
|
735
749
|
// Emit database-live event (fire-and-forget)
|
|
736
|
-
|
|
750
|
+
scheduleDbLive(
|
|
751
|
+
c.executionCtx,
|
|
737
752
|
emitDbLiveEvent(c.env, resolved.namespace, tableName, 'modified', id, updated),
|
|
753
|
+
`emit modified ${resolved.namespace}.${tableName}:${id}`,
|
|
738
754
|
);
|
|
739
755
|
c.executionCtx.waitUntil(
|
|
740
756
|
executeDbTriggers(
|
|
@@ -817,8 +833,10 @@ async function handleDelete(
|
|
|
817
833
|
}
|
|
818
834
|
|
|
819
835
|
// Emit database-live event (fire-and-forget)
|
|
820
|
-
|
|
836
|
+
scheduleDbLive(
|
|
837
|
+
c.executionCtx,
|
|
821
838
|
emitDbLiveEvent(c.env, resolved.namespace, tableName, 'removed', id, stripInternalPgFields(existingRow)),
|
|
839
|
+
`emit removed ${resolved.namespace}.${tableName}:${id}`,
|
|
822
840
|
);
|
|
823
841
|
c.executionCtx.waitUntil(
|
|
824
842
|
executeDbTriggers(
|
|
@@ -944,15 +962,21 @@ async function handleBatch(
|
|
|
944
962
|
data: r as Record<string, unknown>,
|
|
945
963
|
}));
|
|
946
964
|
if (changes.length >= 10) {
|
|
947
|
-
|
|
965
|
+
scheduleDbLive(
|
|
966
|
+
c.executionCtx,
|
|
948
967
|
emitDbLiveBatchEvent(c.env, resolved.namespace, tableName, changes),
|
|
968
|
+
`emit batch ${resolved.namespace}.${tableName} (${changes.length} changes)`,
|
|
949
969
|
);
|
|
950
970
|
} else {
|
|
951
|
-
|
|
952
|
-
c.executionCtx
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
971
|
+
scheduleDbLive(
|
|
972
|
+
c.executionCtx,
|
|
973
|
+
Promise.all(
|
|
974
|
+
changes.map((ch) =>
|
|
975
|
+
emitDbLiveEvent(c.env, resolved.namespace, tableName, ch.type, ch.docId, ch.data),
|
|
976
|
+
),
|
|
977
|
+
).then(() => undefined),
|
|
978
|
+
`emit fan-out ${resolved.namespace}.${tableName} (${changes.length} changes)`,
|
|
979
|
+
);
|
|
956
980
|
}
|
|
957
981
|
}
|
|
958
982
|
|
|
@@ -1031,8 +1055,10 @@ async function handleBatchByFilter(
|
|
|
1031
1055
|
succeeded = result.rowCount;
|
|
1032
1056
|
|
|
1033
1057
|
if (succeeded > 0) {
|
|
1034
|
-
|
|
1058
|
+
scheduleDbLive(
|
|
1059
|
+
c.executionCtx,
|
|
1035
1060
|
emitDbLiveEvent(c.env, resolved.namespace, tableName, 'removed', '_bulk', { action: 'delete', count: succeeded }),
|
|
1061
|
+
`emit bulk ${resolved.namespace}.${tableName} (delete)`,
|
|
1036
1062
|
);
|
|
1037
1063
|
}
|
|
1038
1064
|
|
|
@@ -1074,8 +1100,10 @@ async function handleBatchByFilter(
|
|
|
1074
1100
|
succeeded = result.rowCount;
|
|
1075
1101
|
|
|
1076
1102
|
if (succeeded > 0) {
|
|
1077
|
-
|
|
1103
|
+
scheduleDbLive(
|
|
1104
|
+
c.executionCtx,
|
|
1078
1105
|
emitDbLiveEvent(c.env, resolved.namespace, tableName, 'modified', '_bulk', { action: 'update', count: succeeded }),
|
|
1106
|
+
`emit bulk ${resolved.namespace}.${tableName} (update)`,
|
|
1079
1107
|
);
|
|
1080
1108
|
}
|
|
1081
1109
|
|