@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
package/src/lib/functions.ts
CHANGED
|
@@ -25,19 +25,22 @@ import type {
|
|
|
25
25
|
ScheduleTrigger,
|
|
26
26
|
HttpTrigger,
|
|
27
27
|
} from '@edge-base/shared';
|
|
28
|
-
import {
|
|
29
|
-
import { executeDoSql } from './do-sql.js';
|
|
28
|
+
import { getD1BindingName } from './do-router.js';
|
|
30
29
|
import { D1AuthDb, type AuthDb } from './auth-db-adapter.js';
|
|
31
|
-
import {
|
|
32
|
-
import { handlePgRequest } from './postgres-handler.js';
|
|
33
|
-
import { buildInternalHandlerContext } from './internal-request.js';
|
|
30
|
+
import { ensureAuthSchema } from './auth-d1.js';
|
|
34
31
|
import type { Env } from '../types.js';
|
|
35
32
|
import { createSignedToken } from '../routes/storage.js';
|
|
36
33
|
import {
|
|
34
|
+
createManagedAdminUser,
|
|
37
35
|
deleteManagedAdminUser,
|
|
38
36
|
normalizeAdminUserUpdates,
|
|
39
37
|
updateManagedAdminUser,
|
|
40
38
|
} from './admin-user-management.js';
|
|
39
|
+
import { hashPassword } from './password.js';
|
|
40
|
+
import { generateId } from './uuid.js';
|
|
41
|
+
import { DbRef, TableRef, DefaultDbApi, HttpClient, ContextManager } from '@edge-base/core';
|
|
42
|
+
import { InternalHttpTransport } from './internal-transport.js';
|
|
43
|
+
import { executeProviderAwareSql } from './provider-aware-sql.js';
|
|
41
44
|
|
|
42
45
|
// ─── Function Context Types ───
|
|
43
46
|
|
|
@@ -48,21 +51,6 @@ export interface AuthContext {
|
|
|
48
51
|
custom?: Record<string, unknown>;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
export interface TableProxy {
|
|
52
|
-
insert(data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
53
|
-
upsert(
|
|
54
|
-
data: Record<string, unknown>,
|
|
55
|
-
options?: { conflictTarget?: string },
|
|
56
|
-
): Promise<Record<string, unknown>>;
|
|
57
|
-
update(id: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
58
|
-
delete(id: string): Promise<{ deleted: boolean }>;
|
|
59
|
-
get(id: string): Promise<Record<string, unknown>>;
|
|
60
|
-
list(options?: {
|
|
61
|
-
limit?: number;
|
|
62
|
-
filter?: unknown;
|
|
63
|
-
}): Promise<{ items: Record<string, unknown>[] }>;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
54
|
export interface AdminAuthContext {
|
|
67
55
|
getUser(userId: string): Promise<Record<string, unknown>>;
|
|
68
56
|
listUsers(options?: {
|
|
@@ -89,22 +77,41 @@ export interface AdminAuthContext {
|
|
|
89
77
|
*/
|
|
90
78
|
export interface FunctionAdminContext {
|
|
91
79
|
/** Cross-DO table access — rules bypassed, Service Key authenticated. */
|
|
92
|
-
table(name: string):
|
|
80
|
+
table(name: string): TableRef;
|
|
93
81
|
/**
|
|
94
82
|
* Access a specific DB namespace instance (§5).
|
|
95
|
-
*
|
|
83
|
+
* Uses the same TableRef from @edge-base/core as the client SDK,
|
|
84
|
+
* ensuring API parity (getList, getOne, where, orderBy, limit, etc.).
|
|
96
85
|
*
|
|
97
86
|
* @example
|
|
98
87
|
* // Static DB
|
|
99
|
-
* context.admin.db('shared').table('posts').
|
|
88
|
+
* context.admin.db('shared').table('posts').getList()
|
|
100
89
|
* // Dynamic DB (tenant/user)
|
|
101
|
-
* context.admin.db('workspace', 'ws-456').table('documents').
|
|
90
|
+
* context.admin.db('workspace', 'ws-456').table('documents').getList()
|
|
91
|
+
* // With query builder
|
|
92
|
+
* context.admin.db('shared').table('posts').where('status', '==', 'published').limit(10).getList()
|
|
102
93
|
*/
|
|
103
|
-
db(namespace: string, id?: string):
|
|
94
|
+
db(namespace: string, id?: string): DbRef;
|
|
104
95
|
/** Admin user management. */
|
|
105
96
|
auth: AdminAuthContext;
|
|
106
|
-
/**
|
|
107
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Execute raw SQL through the fastest provider-aware path available.
|
|
99
|
+
* Uses direct PostgreSQL / Neon, D1, or DO execution when possible,
|
|
100
|
+
* then falls back to the internal HTTP SQL route when needed.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* const rows = await ctx.admin.sqlProviderAware('shared', undefined, 'SELECT * FROM posts WHERE status = ?', ['published']);
|
|
104
|
+
*/
|
|
105
|
+
sqlProviderAware(
|
|
106
|
+
namespace: string,
|
|
107
|
+
id: string | undefined,
|
|
108
|
+
query: string,
|
|
109
|
+
params?: unknown[],
|
|
110
|
+
): Promise<unknown[]>;
|
|
111
|
+
/**
|
|
112
|
+
* @deprecated Use `sqlProviderAware()` instead.
|
|
113
|
+
*/
|
|
114
|
+
sqlWithDirectD1Access(
|
|
108
115
|
namespace: string,
|
|
109
116
|
id: string | undefined,
|
|
110
117
|
query: string,
|
|
@@ -139,9 +146,7 @@ export interface FunctionPushProxy {
|
|
|
139
146
|
payload: Record<string, unknown>,
|
|
140
147
|
): Promise<{ sent: number; failed: number; removed: number }>;
|
|
141
148
|
/** Get registered device tokens for a user — token values NOT exposed. */
|
|
142
|
-
getTokens(
|
|
143
|
-
userId: string,
|
|
144
|
-
): Promise<
|
|
149
|
+
getTokens(userId: string): Promise<
|
|
145
150
|
Array<{
|
|
146
151
|
deviceId: string;
|
|
147
152
|
platform: string;
|
|
@@ -181,12 +186,30 @@ export interface FunctionPushProxy {
|
|
|
181
186
|
|
|
182
187
|
/** Storage proxy for App Functions — wraps R2Bucket with convenience methods. */
|
|
183
188
|
export interface FunctionStorageProxy {
|
|
184
|
-
put(
|
|
185
|
-
|
|
189
|
+
put(
|
|
190
|
+
key: string,
|
|
191
|
+
value: ReadableStream | ArrayBuffer | string,
|
|
192
|
+
options?: { contentType?: string; customMetadata?: Record<string, string> },
|
|
193
|
+
): Promise<void>;
|
|
194
|
+
get(key: string): Promise<{
|
|
195
|
+
body: ReadableStream;
|
|
196
|
+
contentType: string;
|
|
197
|
+
size: number;
|
|
198
|
+
customMetadata: Record<string, string>;
|
|
199
|
+
} | null>;
|
|
186
200
|
delete(key: string): Promise<void>;
|
|
187
201
|
getSignedUrl(key: string, options?: { expiresIn?: number }): Promise<string>;
|
|
188
|
-
list(options?: { prefix?: string; limit?: number; cursor?: string }): Promise<{
|
|
189
|
-
|
|
202
|
+
list(options?: { prefix?: string; limit?: number; cursor?: string }): Promise<{
|
|
203
|
+
keys: Array<{ key: string; size: number; contentType: string }>;
|
|
204
|
+
cursor?: string;
|
|
205
|
+
truncated: boolean;
|
|
206
|
+
}>;
|
|
207
|
+
head(key: string): Promise<{
|
|
208
|
+
key: string;
|
|
209
|
+
size: number;
|
|
210
|
+
contentType: string;
|
|
211
|
+
customMetadata: Record<string, string>;
|
|
212
|
+
} | null>;
|
|
190
213
|
}
|
|
191
214
|
|
|
192
215
|
/** KV proxy for App Functions — routes through Worker HTTP. */
|
|
@@ -291,20 +314,21 @@ export interface FunctionContext {
|
|
|
291
314
|
*
|
|
292
315
|
* @example
|
|
293
316
|
* // Static DB
|
|
294
|
-
* await context.db('shared').table('posts').
|
|
317
|
+
* await context.db('shared').table('posts').getList()
|
|
295
318
|
* // Dynamic DB
|
|
296
|
-
* await context.db('workspace', 'ws-456').table('documents').
|
|
319
|
+
* await context.db('workspace', 'ws-456').table('documents').getList()
|
|
320
|
+
* // With query builder
|
|
321
|
+
* await context.db('shared').table('posts').where('status', '==', 'published').limit(10).getList()
|
|
297
322
|
*/
|
|
298
323
|
db: FunctionAdminContext['db'];
|
|
299
324
|
/**
|
|
300
|
-
* Server-side EdgeBase admin client (§5
|
|
325
|
+
* Server-side EdgeBase admin client (§5).
|
|
301
326
|
* Use context.admin.db(namespace, id?).table(name) for all DB access.
|
|
327
|
+
* Uses the same TableRef from @edge-base/core as the client SDK.
|
|
302
328
|
*
|
|
303
329
|
* @example
|
|
304
|
-
*
|
|
305
|
-
* await context.admin.db('shared').table('posts').
|
|
306
|
-
* // Dynamic DB
|
|
307
|
-
* await context.admin.db('workspace', 'ws-456').table('documents').list()
|
|
330
|
+
* await context.admin.db('shared').table('posts').getList()
|
|
331
|
+
* await context.admin.db('shared').table('posts').where('userId', '==', uid).getList()
|
|
308
332
|
*/
|
|
309
333
|
admin: FunctionAdminContext;
|
|
310
334
|
/**
|
|
@@ -365,13 +389,14 @@ function getRegistryName(key: string, def: FunctionDefinition): string {
|
|
|
365
389
|
|
|
366
390
|
export function registerFunction(name: string, def: FunctionDefinition): void {
|
|
367
391
|
if (!def || typeof def !== 'object' || !def.trigger) {
|
|
368
|
-
const received =
|
|
369
|
-
|
|
370
|
-
|
|
392
|
+
const received =
|
|
393
|
+
typeof def === 'function'
|
|
394
|
+
? 'a plain function'
|
|
395
|
+
: `${typeof def} (${JSON.stringify(def)?.slice(0, 100)})`;
|
|
371
396
|
throw new Error(
|
|
372
397
|
`registerFunction('${name}'): expected a FunctionDefinition with a 'trigger' property, but received ${received}. ` +
|
|
373
|
-
|
|
374
|
-
|
|
398
|
+
`Functions must use defineFunction() from '@edge-base/shared' and be exported as named HTTP method exports ` +
|
|
399
|
+
`(e.g. export const GET = defineFunction(...)). See https://docs.edgebase.dev/functions for details.`,
|
|
375
400
|
);
|
|
376
401
|
}
|
|
377
402
|
functionRegistry.set(buildRegistryKey(name, def), def);
|
|
@@ -678,7 +703,12 @@ export function wrapMethodExport(
|
|
|
678
703
|
} else if (handler && typeof handler === 'object') {
|
|
679
704
|
fn = (handler.handler ?? handler) as unknown as (ctx: unknown) => Promise<unknown>;
|
|
680
705
|
captcha = handler.captcha;
|
|
681
|
-
if (
|
|
706
|
+
if (
|
|
707
|
+
'trigger' in handler &&
|
|
708
|
+
handler.trigger &&
|
|
709
|
+
typeof handler.trigger === 'object' &&
|
|
710
|
+
'path' in handler.trigger
|
|
711
|
+
) {
|
|
682
712
|
const triggerPath = handler.trigger.path;
|
|
683
713
|
path = typeof triggerPath === 'string' ? triggerPath : undefined;
|
|
684
714
|
}
|
|
@@ -697,284 +727,7 @@ export function wrapMethodExport(
|
|
|
697
727
|
};
|
|
698
728
|
}
|
|
699
729
|
|
|
700
|
-
// ───
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* Build a cross-DO table proxy.
|
|
704
|
-
* All calls bypass access rules by using Service Key.
|
|
705
|
-
*/
|
|
706
|
-
function buildTableProxy(
|
|
707
|
-
tableName: string,
|
|
708
|
-
namespace: DurableObjectNamespace,
|
|
709
|
-
config: EdgeBaseConfig,
|
|
710
|
-
workerUrl?: string,
|
|
711
|
-
serviceKey?: string,
|
|
712
|
-
env?: Env,
|
|
713
|
-
executionCtx?: ExecutionContext,
|
|
714
|
-
/** DB routing info (§5). If provided, overrides auto-detect. */
|
|
715
|
-
dbTarget?: { namespace: string; id?: string; doName: string },
|
|
716
|
-
routingOptions?: { preferDirectDo?: boolean },
|
|
717
|
-
): TableProxy {
|
|
718
|
-
// Determine DO name: explicit DB target (§5) or auto-detect from config
|
|
719
|
-
let resolvedTarget: { namespace: string; id?: string; doName: string };
|
|
720
|
-
if (dbTarget) {
|
|
721
|
-
resolvedTarget = dbTarget;
|
|
722
|
-
} else {
|
|
723
|
-
// Auto-detect namespace for this table from config (§1)
|
|
724
|
-
let tableNamespace = 'shared';
|
|
725
|
-
for (const [ns, dbBlock] of Object.entries(config.databases ?? {})) {
|
|
726
|
-
if (dbBlock.tables?.[tableName]) {
|
|
727
|
-
tableNamespace = ns;
|
|
728
|
-
break;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
resolvedTarget = {
|
|
732
|
-
namespace: tableNamespace,
|
|
733
|
-
doName: getDbDoName(tableNamespace),
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
const doName = resolvedTarget.doName;
|
|
737
|
-
const headers: Record<string, string> = { 'X-DO-Name': doName };
|
|
738
|
-
if (serviceKey) {
|
|
739
|
-
headers['X-EdgeBase-Service-Key'] = serviceKey;
|
|
740
|
-
}
|
|
741
|
-
// Add internal header to bypass rules
|
|
742
|
-
headers['X-EdgeBase-Internal'] = 'true';
|
|
743
|
-
|
|
744
|
-
const buildTablePath = (id?: string, query?: URLSearchParams): string => {
|
|
745
|
-
const base = resolvedTarget.id !== undefined
|
|
746
|
-
? `/api/db/${resolvedTarget.namespace}/${resolvedTarget.id}/tables/${tableName}`
|
|
747
|
-
: `/api/db/${resolvedTarget.namespace}/tables/${tableName}`;
|
|
748
|
-
const withId = id ? `${base}/${id}` : base;
|
|
749
|
-
const search = query && Array.from(query.keys()).length > 0 ? `?${query.toString()}` : '';
|
|
750
|
-
return `${withId}${search}`;
|
|
751
|
-
};
|
|
752
|
-
const buildDirectTablePath = (id?: string, query?: URLSearchParams): string => {
|
|
753
|
-
const base = `/tables/${tableName}`;
|
|
754
|
-
const withId = id ? `${base}/${id}` : base;
|
|
755
|
-
const search = query && Array.from(query.keys()).length > 0 ? `?${query.toString()}` : '';
|
|
756
|
-
return `${withId}${search}`;
|
|
757
|
-
};
|
|
758
|
-
|
|
759
|
-
const requestViaWorker = async (
|
|
760
|
-
method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
|
|
761
|
-
path: string,
|
|
762
|
-
body?: Record<string, unknown>,
|
|
763
|
-
): Promise<Response> => {
|
|
764
|
-
return fetch(`${workerUrl}${path}`, {
|
|
765
|
-
method,
|
|
766
|
-
headers: {
|
|
767
|
-
'Content-Type': 'application/json',
|
|
768
|
-
...headers,
|
|
769
|
-
},
|
|
770
|
-
body: body === undefined || method === 'GET' || method === 'DELETE'
|
|
771
|
-
? undefined
|
|
772
|
-
: JSON.stringify(body),
|
|
773
|
-
});
|
|
774
|
-
};
|
|
775
|
-
const requestViaD1Handler = async (
|
|
776
|
-
method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
|
|
777
|
-
directPath: string,
|
|
778
|
-
query?: URLSearchParams,
|
|
779
|
-
body?: Record<string, unknown>,
|
|
780
|
-
): Promise<Response> => {
|
|
781
|
-
if (!env) {
|
|
782
|
-
throw new Error('D1 table proxy requires env.');
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
const request = new Request(
|
|
786
|
-
`http://internal/api/db/${resolvedTarget.namespace}${resolvedTarget.id ? `/${resolvedTarget.id}` : ''}${buildDirectTablePath(undefined, query).replace('/tables/' + tableName, directPath)}`,
|
|
787
|
-
{
|
|
788
|
-
method,
|
|
789
|
-
headers: {
|
|
790
|
-
'Content-Type': 'application/json',
|
|
791
|
-
...headers,
|
|
792
|
-
},
|
|
793
|
-
body: body === undefined || method === 'GET' || method === 'DELETE'
|
|
794
|
-
? undefined
|
|
795
|
-
: JSON.stringify(body),
|
|
796
|
-
},
|
|
797
|
-
);
|
|
798
|
-
|
|
799
|
-
return handleD1Request(
|
|
800
|
-
buildInternalHandlerContext({
|
|
801
|
-
env,
|
|
802
|
-
request,
|
|
803
|
-
body,
|
|
804
|
-
executionCtx,
|
|
805
|
-
}),
|
|
806
|
-
resolvedTarget.namespace,
|
|
807
|
-
tableName,
|
|
808
|
-
directPath,
|
|
809
|
-
);
|
|
810
|
-
};
|
|
811
|
-
const requestViaPgHandler = async (
|
|
812
|
-
method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
|
|
813
|
-
directPath: string,
|
|
814
|
-
query?: URLSearchParams,
|
|
815
|
-
body?: Record<string, unknown>,
|
|
816
|
-
): Promise<Response> => {
|
|
817
|
-
if (!env) {
|
|
818
|
-
throw new Error('PostgreSQL table proxy requires env.');
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
const request = new Request(
|
|
822
|
-
`http://internal/api/db/${resolvedTarget.namespace}${resolvedTarget.id ? `/${resolvedTarget.id}` : ''}${buildDirectTablePath(undefined, query).replace('/tables/' + tableName, directPath)}`,
|
|
823
|
-
{
|
|
824
|
-
method,
|
|
825
|
-
headers: {
|
|
826
|
-
'Content-Type': 'application/json',
|
|
827
|
-
...headers,
|
|
828
|
-
},
|
|
829
|
-
body: body === undefined || method === 'GET' || method === 'DELETE'
|
|
830
|
-
? undefined
|
|
831
|
-
: JSON.stringify(body),
|
|
832
|
-
},
|
|
833
|
-
);
|
|
834
|
-
|
|
835
|
-
return handlePgRequest(
|
|
836
|
-
buildInternalHandlerContext({
|
|
837
|
-
env,
|
|
838
|
-
request,
|
|
839
|
-
body,
|
|
840
|
-
executionCtx,
|
|
841
|
-
}),
|
|
842
|
-
resolvedTarget.namespace,
|
|
843
|
-
tableName,
|
|
844
|
-
directPath,
|
|
845
|
-
);
|
|
846
|
-
};
|
|
847
|
-
const requestViaDirectDo = async (
|
|
848
|
-
method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
|
|
849
|
-
directPath: string,
|
|
850
|
-
body?: Record<string, unknown>,
|
|
851
|
-
): Promise<Response> => {
|
|
852
|
-
const res = await callDO(namespace, doName, directPath, {
|
|
853
|
-
method,
|
|
854
|
-
body,
|
|
855
|
-
headers,
|
|
856
|
-
});
|
|
857
|
-
|
|
858
|
-
if (!resolvedTarget.id || res.status !== 201) {
|
|
859
|
-
return res;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
const createPayload = await res.clone().json().catch(() => null) as
|
|
863
|
-
| { needsCreate?: boolean }
|
|
864
|
-
| null;
|
|
865
|
-
if (!createPayload?.needsCreate) {
|
|
866
|
-
return res;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
return callDO(namespace, doName, directPath, {
|
|
870
|
-
method,
|
|
871
|
-
body,
|
|
872
|
-
headers: {
|
|
873
|
-
...headers,
|
|
874
|
-
'X-DO-Create-Authorized': '1',
|
|
875
|
-
},
|
|
876
|
-
});
|
|
877
|
-
};
|
|
878
|
-
const requestTable = async (
|
|
879
|
-
method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
|
|
880
|
-
id?: string,
|
|
881
|
-
body?: Record<string, unknown>,
|
|
882
|
-
query?: URLSearchParams,
|
|
883
|
-
): Promise<Response> => {
|
|
884
|
-
const directPath = buildDirectTablePath(id);
|
|
885
|
-
const directPathWithQuery = buildDirectTablePath(id, query);
|
|
886
|
-
const provider = config.databases?.[resolvedTarget.namespace]?.provider;
|
|
887
|
-
|
|
888
|
-
if (!routingOptions?.preferDirectDo && shouldRouteToD1(resolvedTarget.namespace, config) && env) {
|
|
889
|
-
return requestViaD1Handler(method, directPath, query, body);
|
|
890
|
-
}
|
|
891
|
-
if ((provider === 'neon' || provider === 'postgres') && env) {
|
|
892
|
-
return requestViaPgHandler(method, directPath, query, body);
|
|
893
|
-
}
|
|
894
|
-
if (env) {
|
|
895
|
-
return requestViaDirectDo(method, directPathWithQuery, body);
|
|
896
|
-
}
|
|
897
|
-
if (workerUrl) {
|
|
898
|
-
return requestViaWorker(method, buildTablePath(id, query), body);
|
|
899
|
-
}
|
|
900
|
-
return requestViaDirectDo(method, directPathWithQuery, body);
|
|
901
|
-
};
|
|
902
|
-
|
|
903
|
-
const insert = async (data: Record<string, unknown>): Promise<Record<string, unknown>> => {
|
|
904
|
-
const res = await requestTable('POST', undefined, data);
|
|
905
|
-
if (!res.ok) {
|
|
906
|
-
const err = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
907
|
-
throw new Error(`Cross-DO insert failed: ${err.message || res.status}`);
|
|
908
|
-
}
|
|
909
|
-
return (await res.json()) as Record<string, unknown>;
|
|
910
|
-
};
|
|
911
|
-
|
|
912
|
-
const upsert = async (
|
|
913
|
-
data: Record<string, unknown>,
|
|
914
|
-
options?: { conflictTarget?: string },
|
|
915
|
-
): Promise<Record<string, unknown>> => {
|
|
916
|
-
const query = new URLSearchParams();
|
|
917
|
-
query.set('upsert', 'true');
|
|
918
|
-
if (options?.conflictTarget) {
|
|
919
|
-
query.set('conflictTarget', options.conflictTarget);
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
const res = await requestTable('POST', undefined, data, query);
|
|
923
|
-
if (!res.ok) {
|
|
924
|
-
const err = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
925
|
-
throw new Error(`Cross-DO upsert failed: ${err.message || res.status}`);
|
|
926
|
-
}
|
|
927
|
-
return (await res.json()) as Record<string, unknown>;
|
|
928
|
-
};
|
|
929
|
-
|
|
930
|
-
const update = async (
|
|
931
|
-
id: string,
|
|
932
|
-
data: Record<string, unknown>,
|
|
933
|
-
): Promise<Record<string, unknown>> => {
|
|
934
|
-
const res = await requestTable('PATCH', id, data);
|
|
935
|
-
if (!res.ok) {
|
|
936
|
-
const err = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
937
|
-
throw new Error(`Cross-DO update failed: ${err.message || res.status}`);
|
|
938
|
-
}
|
|
939
|
-
return (await res.json()) as Record<string, unknown>;
|
|
940
|
-
};
|
|
941
|
-
|
|
942
|
-
const del = async (id: string): Promise<{ deleted: boolean }> => {
|
|
943
|
-
const res = await requestTable('DELETE', id);
|
|
944
|
-
if (!res.ok) {
|
|
945
|
-
const err = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
946
|
-
throw new Error(`Cross-DO delete failed: ${err.message || res.status}`);
|
|
947
|
-
}
|
|
948
|
-
return (await res.json()) as { deleted: boolean };
|
|
949
|
-
};
|
|
950
|
-
|
|
951
|
-
const get = async (id: string): Promise<Record<string, unknown>> => {
|
|
952
|
-
const res = await requestTable('GET', id);
|
|
953
|
-
if (!res.ok) {
|
|
954
|
-
const err = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
955
|
-
throw new Error(`Cross-DO get failed: ${err.message || res.status}`);
|
|
956
|
-
}
|
|
957
|
-
return (await res.json()) as Record<string, unknown>;
|
|
958
|
-
};
|
|
959
|
-
|
|
960
|
-
const list = async (listOptions?: {
|
|
961
|
-
limit?: number;
|
|
962
|
-
filter?: unknown;
|
|
963
|
-
}): Promise<{ items: Record<string, unknown>[] }> => {
|
|
964
|
-
const query = new URLSearchParams();
|
|
965
|
-
if (listOptions?.limit) query.set('limit', String(listOptions.limit));
|
|
966
|
-
if (listOptions?.filter) query.set('filter', JSON.stringify(listOptions.filter));
|
|
967
|
-
|
|
968
|
-
const res = await requestTable('GET', undefined, undefined, query);
|
|
969
|
-
if (!res.ok) {
|
|
970
|
-
const err = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
971
|
-
throw new Error(`Cross-DO list failed: ${err.message || res.status}`);
|
|
972
|
-
}
|
|
973
|
-
return (await res.json()) as { items: Record<string, unknown>[] };
|
|
974
|
-
};
|
|
975
|
-
|
|
976
|
-
return { insert, upsert, update, delete: del, get, list };
|
|
977
|
-
}
|
|
730
|
+
// ─── Admin DB Proxy (uses @edge-base/core TableRef via InternalHttpTransport) ───
|
|
978
731
|
|
|
979
732
|
interface BuildAdminDbProxyOptions {
|
|
980
733
|
databaseNamespace: DurableObjectNamespace;
|
|
@@ -986,25 +739,111 @@ interface BuildAdminDbProxyOptions {
|
|
|
986
739
|
preferDirectDo?: boolean;
|
|
987
740
|
}
|
|
988
741
|
|
|
742
|
+
// ─── Shared SQL executor — provider-aware direct paths → HTTP fallback ───
|
|
743
|
+
|
|
744
|
+
export interface SqlProviderAwareOptions {
|
|
745
|
+
env?: Env;
|
|
746
|
+
config: EdgeBaseConfig;
|
|
747
|
+
databaseNamespace?: DurableObjectNamespace;
|
|
748
|
+
workerUrl?: string;
|
|
749
|
+
serviceKey?: string;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Execute raw SQL with the fastest provider-aware path:
|
|
754
|
+
* 1. PostgreSQL / Neon direct query path
|
|
755
|
+
* 2. D1 direct binding
|
|
756
|
+
* 3. Durable Object direct call
|
|
757
|
+
* 4. HTTP fallback via workerUrl
|
|
758
|
+
*
|
|
759
|
+
* Shared by buildFunctionContext, auth hooks, storage hooks, and plugin migrations.
|
|
760
|
+
*/
|
|
761
|
+
export async function executeSqlProviderAware(
|
|
762
|
+
opts: SqlProviderAwareOptions,
|
|
763
|
+
namespace: string,
|
|
764
|
+
id: string | undefined,
|
|
765
|
+
query: string,
|
|
766
|
+
params?: unknown[],
|
|
767
|
+
): Promise<unknown[]> {
|
|
768
|
+
const result = await executeProviderAwareSql(
|
|
769
|
+
{
|
|
770
|
+
env: opts.env,
|
|
771
|
+
config: opts.config,
|
|
772
|
+
databaseNamespace: opts.databaseNamespace,
|
|
773
|
+
workerUrl: opts.workerUrl,
|
|
774
|
+
serviceKey: opts.serviceKey,
|
|
775
|
+
},
|
|
776
|
+
namespace,
|
|
777
|
+
id,
|
|
778
|
+
query,
|
|
779
|
+
params ?? [],
|
|
780
|
+
);
|
|
781
|
+
return result.rows;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Backwards-compatible aliases for existing internal callers and public surfaces.
|
|
785
|
+
export const executeSqlWithDirectBindingAccess = executeSqlProviderAware;
|
|
786
|
+
export const executeSqlWithDirectD1Access = executeSqlProviderAware;
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Build the admin DB proxy that returns real DbRef/TableRef instances
|
|
790
|
+
* from @edge-base/core, routed through InternalHttpTransport for
|
|
791
|
+
* direct D1/PG/DO access (no HTTP round-trip).
|
|
792
|
+
*/
|
|
989
793
|
export function buildAdminDbProxy(options: BuildAdminDbProxyOptions): FunctionAdminContext['db'] {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
794
|
+
// Create HttpClient fallback for sql() tagged template support on TableRef.
|
|
795
|
+
// Trusted server contexts also receive a direct SQL executor below.
|
|
796
|
+
let httpClient: HttpClient | undefined;
|
|
797
|
+
if (options.workerUrl) {
|
|
798
|
+
httpClient = new HttpClient({
|
|
799
|
+
baseUrl: options.workerUrl,
|
|
800
|
+
serviceKey: options.serviceKey,
|
|
801
|
+
contextManager: new ContextManager(),
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
const sqlExecutor = (
|
|
805
|
+
namespace: string,
|
|
806
|
+
id: string | undefined,
|
|
807
|
+
query: string,
|
|
808
|
+
params?: unknown[],
|
|
809
|
+
) =>
|
|
810
|
+
executeSqlProviderAware(
|
|
811
|
+
{
|
|
812
|
+
env: options.env,
|
|
813
|
+
config: options.config,
|
|
814
|
+
databaseNamespace: options.databaseNamespace,
|
|
815
|
+
workerUrl: options.workerUrl,
|
|
816
|
+
serviceKey: options.serviceKey,
|
|
1006
817
|
},
|
|
1007
|
-
|
|
818
|
+
namespace,
|
|
819
|
+
id,
|
|
820
|
+
query,
|
|
821
|
+
params,
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
return (namespace: string, id?: string): DbRef => {
|
|
825
|
+
// Create a per-DbRef transport with explicit dbContext so that
|
|
826
|
+
// path parsing is unambiguous even when instanceId === 'tables'.
|
|
827
|
+
const transport = new InternalHttpTransport({
|
|
828
|
+
databaseNamespace: options.databaseNamespace,
|
|
829
|
+
config: options.config,
|
|
830
|
+
workerUrl: options.workerUrl,
|
|
831
|
+
serviceKey: options.serviceKey,
|
|
832
|
+
env: options.env,
|
|
833
|
+
executionCtx: options.executionCtx,
|
|
834
|
+
preferDirectDo: options.preferDirectDo,
|
|
835
|
+
dbContext: { namespace, instanceId: id },
|
|
836
|
+
});
|
|
837
|
+
const dbApi = new DefaultDbApi(transport);
|
|
838
|
+
return new DbRef(
|
|
839
|
+
dbApi,
|
|
840
|
+
namespace,
|
|
841
|
+
id,
|
|
842
|
+
undefined, // databaseLiveClient — not available server-side
|
|
843
|
+
undefined, // filterMatchFn
|
|
844
|
+
httpClient, // enables table().sql`...` tagged template
|
|
845
|
+
sqlExecutor,
|
|
846
|
+
);
|
|
1008
847
|
};
|
|
1009
848
|
}
|
|
1010
849
|
|
|
@@ -1024,7 +863,7 @@ interface AdminAuthOptions {
|
|
|
1024
863
|
/**
|
|
1025
864
|
* Build admin auth context for App Functions.
|
|
1026
865
|
* Uses AUTH_DB D1 directly for all operations (D1-first architecture).
|
|
1027
|
-
* Cross-shard operations (listUsers
|
|
866
|
+
* Cross-shard operations (listUsers) also available via Worker HTTP relay
|
|
1028
867
|
* when workerUrl is provided.
|
|
1029
868
|
*/
|
|
1030
869
|
export function buildAdminAuthContext(options: AdminAuthOptions): AdminAuthContext {
|
|
@@ -1089,10 +928,51 @@ export function buildAdminAuthContext(options: AdminAuthOptions): AdminAuthConte
|
|
|
1089
928
|
displayName?: string;
|
|
1090
929
|
role?: string;
|
|
1091
930
|
}): Promise<Record<string, unknown>> {
|
|
931
|
+
// Direct D1 path — works without service key (same as updateUser/deleteUser)
|
|
932
|
+
if (d1Database) {
|
|
933
|
+
// Input validation (mirrors routes/admin-auth.ts guards)
|
|
934
|
+
if (!data.email || typeof data.email !== 'string')
|
|
935
|
+
throw new Error('Email and password are required.');
|
|
936
|
+
if (!data.password || typeof data.password !== 'string')
|
|
937
|
+
throw new Error('Email and password are required.');
|
|
938
|
+
const email = data.email.trim().toLowerCase();
|
|
939
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
940
|
+
throw new Error('Invalid email format.');
|
|
941
|
+
}
|
|
942
|
+
if (data.password.length < 8) throw new Error('Password must be at least 8 characters.');
|
|
943
|
+
if (data.password.length > 256) throw new Error('Password must not exceed 256 characters.');
|
|
944
|
+
if (data.displayName && data.displayName.length > 200) {
|
|
945
|
+
throw new Error('Display name must not exceed 200 characters.');
|
|
946
|
+
}
|
|
947
|
+
// Role validation (mirrors normalizeOptionalRole in routes/admin-auth.ts)
|
|
948
|
+
let role = 'user';
|
|
949
|
+
if (data.role !== undefined) {
|
|
950
|
+
if (typeof data.role !== 'string') throw new Error('Role must be a non-empty string.');
|
|
951
|
+
const trimmed = data.role.trim();
|
|
952
|
+
if (!trimmed) throw new Error('Role must be a non-empty string.');
|
|
953
|
+
if (trimmed.length > 100) throw new Error('Role must not exceed 100 characters.');
|
|
954
|
+
role = trimmed;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const db = new D1AuthDb(d1Database);
|
|
958
|
+
// Ensure auth tables exist (critical for fresh databases)
|
|
959
|
+
await ensureAuthSchema(db);
|
|
960
|
+
const user = await createManagedAdminUser(
|
|
961
|
+
db,
|
|
962
|
+
{
|
|
963
|
+
userId: generateId(),
|
|
964
|
+
email,
|
|
965
|
+
passwordHash: await hashPassword(data.password),
|
|
966
|
+
displayName: data.displayName,
|
|
967
|
+
role,
|
|
968
|
+
verified: true,
|
|
969
|
+
},
|
|
970
|
+
{ kv: kvNamespace },
|
|
971
|
+
);
|
|
972
|
+
return authService.sanitizeUser(user, { includeAppMetadata: true });
|
|
973
|
+
}
|
|
1092
974
|
if (workerUrl && serviceKey) {
|
|
1093
|
-
// HTTP relay: POST /api/auth/admin/users → Worker → D1
|
|
1094
|
-
// createUser has complex side effects (email index, _users_public, etc.)
|
|
1095
|
-
// so it routes through the admin route which handles the full flow.
|
|
975
|
+
// HTTP relay fallback: POST /api/auth/admin/users → Worker → D1
|
|
1096
976
|
const res = await fetch(`${workerUrl}/api/auth/admin/users`, {
|
|
1097
977
|
method: 'POST',
|
|
1098
978
|
headers: {
|
|
@@ -1111,7 +991,7 @@ export function buildAdminAuthContext(options: AdminAuthOptions): AdminAuthConte
|
|
|
1111
991
|
return result.user;
|
|
1112
992
|
}
|
|
1113
993
|
throw new Error(
|
|
1114
|
-
'admin.auth.createUser() is not available in this context (requires workerUrl). ' +
|
|
994
|
+
'admin.auth.createUser() is not available in this context (requires D1 or workerUrl). ' +
|
|
1115
995
|
'Pass workerUrl to buildFunctionContext(), or use the external SDK.',
|
|
1116
996
|
);
|
|
1117
997
|
},
|
|
@@ -1215,6 +1095,25 @@ export function buildFunctionContext(options: BuildFunctionContextOptions): Func
|
|
|
1215
1095
|
executionCtx: options.executionCtx,
|
|
1216
1096
|
preferDirectDo: options.preferDirectDoDb,
|
|
1217
1097
|
});
|
|
1098
|
+
const sqlProviderAware = (
|
|
1099
|
+
namespace: string,
|
|
1100
|
+
id: string | undefined,
|
|
1101
|
+
query: string,
|
|
1102
|
+
params?: unknown[],
|
|
1103
|
+
) =>
|
|
1104
|
+
executeSqlProviderAware(
|
|
1105
|
+
{
|
|
1106
|
+
env: options.env,
|
|
1107
|
+
config: options.config,
|
|
1108
|
+
databaseNamespace: options.databaseNamespace,
|
|
1109
|
+
workerUrl: options.workerUrl,
|
|
1110
|
+
serviceKey: options.serviceKey,
|
|
1111
|
+
},
|
|
1112
|
+
namespace,
|
|
1113
|
+
id,
|
|
1114
|
+
query,
|
|
1115
|
+
params,
|
|
1116
|
+
);
|
|
1218
1117
|
|
|
1219
1118
|
// ─── context.admin — AdminEdgeBase-shaped internal proxy ───
|
|
1220
1119
|
const admin: FunctionAdminContext = {
|
|
@@ -1223,77 +1122,9 @@ export function buildFunctionContext(options: BuildFunctionContextOptions): Func
|
|
|
1223
1122
|
// ─── context.admin.db(namespace, id) — DB-first tenant access (§5) ───
|
|
1224
1123
|
db: adminDb,
|
|
1225
1124
|
auth: adminAuthContext,
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
query: string,
|
|
1230
|
-
params?: unknown[],
|
|
1231
|
-
): Promise<unknown[]> {
|
|
1232
|
-
if (options.env) {
|
|
1233
|
-
const dbBlock = options.config.databases?.[namespace];
|
|
1234
|
-
const isDynamicNamespace = !!(dbBlock?.instance || dbBlock?.access?.canCreate || dbBlock?.access?.access);
|
|
1235
|
-
if (isDynamicNamespace && !id) {
|
|
1236
|
-
throw new Error(`admin.sql() requires an id for dynamic namespace '${namespace}'.`);
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
if (!id && shouldRouteToD1(namespace, options.config)) {
|
|
1240
|
-
const bindingName = getD1BindingName(namespace);
|
|
1241
|
-
const d1 = (options.env as unknown as Record<string, unknown>)[bindingName] as D1Database | undefined;
|
|
1242
|
-
if (!d1) {
|
|
1243
|
-
throw new Error(`D1 binding '${bindingName}' not found.`);
|
|
1244
|
-
}
|
|
1245
|
-
try {
|
|
1246
|
-
const stmt = d1.prepare(query);
|
|
1247
|
-
const bound = params && params.length > 0 ? stmt.bind(...params) : stmt;
|
|
1248
|
-
const result = await bound.all();
|
|
1249
|
-
const rows = (result.results ?? []) as unknown[];
|
|
1250
|
-
return rows;
|
|
1251
|
-
} catch (error) {
|
|
1252
|
-
const message = error instanceof Error ? error.message : 'SQL execution failed';
|
|
1253
|
-
throw new Error(message);
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
return executeDoSql({
|
|
1258
|
-
databaseNamespace: options.databaseNamespace,
|
|
1259
|
-
namespace,
|
|
1260
|
-
id,
|
|
1261
|
-
query,
|
|
1262
|
-
params: params ?? [],
|
|
1263
|
-
internal: true,
|
|
1264
|
-
});
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
if (options.workerUrl && options.serviceKey) {
|
|
1268
|
-
// HTTP route: POST /api/sql → Worker → DatabaseDO (§11)
|
|
1269
|
-
const res = await fetch(`${options.workerUrl}/api/sql`, {
|
|
1270
|
-
method: 'POST',
|
|
1271
|
-
headers: {
|
|
1272
|
-
'Content-Type': 'application/json',
|
|
1273
|
-
'X-EdgeBase-Service-Key': options.serviceKey,
|
|
1274
|
-
},
|
|
1275
|
-
body: JSON.stringify({ namespace, id, sql: query, params: params ?? [] }),
|
|
1276
|
-
});
|
|
1277
|
-
if (!res.ok) {
|
|
1278
|
-
const err = (await res.json().catch(() => ({ message: 'SQL execution failed' }))) as {
|
|
1279
|
-
message: string;
|
|
1280
|
-
};
|
|
1281
|
-
throw new Error(err.message);
|
|
1282
|
-
}
|
|
1283
|
-
const data = (await res.json()) as {
|
|
1284
|
-
rows?: unknown[];
|
|
1285
|
-
items?: unknown[];
|
|
1286
|
-
results?: unknown[];
|
|
1287
|
-
};
|
|
1288
|
-
if (Array.isArray(data.rows)) return data.rows;
|
|
1289
|
-
if (Array.isArray(data.items)) return data.items;
|
|
1290
|
-
if (Array.isArray(data.results)) return data.results;
|
|
1291
|
-
return [];
|
|
1292
|
-
}
|
|
1293
|
-
throw new Error(
|
|
1294
|
-
'admin.sql() requires workerUrl. Pass workerUrl to buildFunctionContext(), or use the external SDK.',
|
|
1295
|
-
);
|
|
1296
|
-
},
|
|
1125
|
+
// ─── Direct provider-aware SQL — delegates to shared executor ───
|
|
1126
|
+
sqlProviderAware,
|
|
1127
|
+
sqlWithDirectD1Access: sqlProviderAware,
|
|
1297
1128
|
async broadcast(
|
|
1298
1129
|
channel: string,
|
|
1299
1130
|
event: string,
|
|
@@ -1302,11 +1133,13 @@ export function buildFunctionContext(options: BuildFunctionContextOptions): Func
|
|
|
1302
1133
|
if (options.env?.DATABASE_LIVE) {
|
|
1303
1134
|
const hubId = options.env.DATABASE_LIVE.idFromName('database-live:hub');
|
|
1304
1135
|
const stub = options.env.DATABASE_LIVE.get(hubId);
|
|
1305
|
-
const response = await stub.fetch(
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1136
|
+
const response = await stub.fetch(
|
|
1137
|
+
new Request('http://do/internal/broadcast', {
|
|
1138
|
+
method: 'POST',
|
|
1139
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1140
|
+
body: JSON.stringify({ channel, event, payload: payload ?? {} }),
|
|
1141
|
+
}),
|
|
1142
|
+
);
|
|
1310
1143
|
if (!response.ok) {
|
|
1311
1144
|
throw new Error(`client.broadcast() failed: ${response.status}`);
|
|
1312
1145
|
}
|
|
@@ -1354,9 +1187,7 @@ export function buildFunctionContext(options: BuildFunctionContextOptions): Func
|
|
|
1354
1187
|
method: 'POST',
|
|
1355
1188
|
headers: {
|
|
1356
1189
|
'Content-Type': 'application/json',
|
|
1357
|
-
...(options.serviceKey
|
|
1358
|
-
? { 'X-EdgeBase-Service-Key': options.serviceKey }
|
|
1359
|
-
: {}),
|
|
1190
|
+
...(options.serviceKey ? { 'X-EdgeBase-Service-Key': options.serviceKey } : {}),
|
|
1360
1191
|
'X-EdgeBase-Call-Depth': String(currentDepth + 1),
|
|
1361
1192
|
},
|
|
1362
1193
|
body: JSON.stringify(data ?? {}),
|
|
@@ -1414,13 +1245,31 @@ export function buildFunctionContext(options: BuildFunctionContextOptions): Func
|
|
|
1414
1245
|
|
|
1415
1246
|
// KV / D1 / Vectorize proxies
|
|
1416
1247
|
kv(namespace: string): FunctionKvProxy {
|
|
1417
|
-
return buildFunctionKvProxy(
|
|
1248
|
+
return buildFunctionKvProxy(
|
|
1249
|
+
namespace,
|
|
1250
|
+
options.config,
|
|
1251
|
+
options.env,
|
|
1252
|
+
options.workerUrl,
|
|
1253
|
+
options.serviceKey,
|
|
1254
|
+
);
|
|
1418
1255
|
},
|
|
1419
1256
|
d1(database: string): FunctionD1Proxy {
|
|
1420
|
-
return buildFunctionD1Proxy(
|
|
1257
|
+
return buildFunctionD1Proxy(
|
|
1258
|
+
database,
|
|
1259
|
+
options.config,
|
|
1260
|
+
options.env,
|
|
1261
|
+
options.workerUrl,
|
|
1262
|
+
options.serviceKey,
|
|
1263
|
+
);
|
|
1421
1264
|
},
|
|
1422
1265
|
vector(index: string): FunctionVectorizeProxy {
|
|
1423
|
-
return buildFunctionVectorizeProxy(
|
|
1266
|
+
return buildFunctionVectorizeProxy(
|
|
1267
|
+
index,
|
|
1268
|
+
options.config,
|
|
1269
|
+
options.env,
|
|
1270
|
+
options.workerUrl,
|
|
1271
|
+
options.serviceKey,
|
|
1272
|
+
);
|
|
1424
1273
|
},
|
|
1425
1274
|
|
|
1426
1275
|
// Push notification management
|
|
@@ -1659,11 +1508,14 @@ export function buildFunctionD1Proxy(
|
|
|
1659
1508
|
return {
|
|
1660
1509
|
async exec<T = Record<string, unknown>>(query: string, params?: unknown[]): Promise<T[]> {
|
|
1661
1510
|
if (config && env) {
|
|
1662
|
-
const bindingName =
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1511
|
+
const bindingName =
|
|
1512
|
+
config.d1?.[database]?.binding ??
|
|
1513
|
+
(database === 'auth' ? 'AUTH_DB' : undefined) ??
|
|
1514
|
+
(database === 'control' ? 'CONTROL_DB' : undefined) ??
|
|
1515
|
+
getD1BindingName(database);
|
|
1516
|
+
const binding = (env as unknown as Record<string, unknown>)[bindingName] as
|
|
1517
|
+
| D1Database
|
|
1518
|
+
| undefined;
|
|
1667
1519
|
if (!binding) {
|
|
1668
1520
|
throw new Error(`D1 binding '${bindingName}' not found.`);
|
|
1669
1521
|
}
|
|
@@ -1715,7 +1567,7 @@ export function buildFunctionVectorizeProxy(
|
|
|
1715
1567
|
if (values instanceof Float32Array || values instanceof Float64Array) {
|
|
1716
1568
|
return Array.from(values);
|
|
1717
1569
|
}
|
|
1718
|
-
return Array.isArray(values) ? values as number[] : undefined;
|
|
1570
|
+
return Array.isArray(values) ? (values as number[]) : undefined;
|
|
1719
1571
|
};
|
|
1720
1572
|
|
|
1721
1573
|
const mapMatches = (
|
|
@@ -1726,15 +1578,19 @@ export function buildFunctionVectorizeProxy(
|
|
|
1726
1578
|
metadata?: Record<string, unknown>;
|
|
1727
1579
|
namespace?: string;
|
|
1728
1580
|
}>,
|
|
1729
|
-
) =>
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1581
|
+
) =>
|
|
1582
|
+
matches.map((match) => ({
|
|
1583
|
+
id: match.id,
|
|
1584
|
+
score: match.score,
|
|
1585
|
+
...(match.values !== undefined ? { values: normalizeValues(match.values) } : {}),
|
|
1586
|
+
...(match.metadata !== undefined ? { metadata: match.metadata } : {}),
|
|
1587
|
+
...(match.namespace ? { namespace: match.namespace } : {}),
|
|
1588
|
+
}));
|
|
1589
|
+
|
|
1590
|
+
const withNamespace = <T extends { namespace?: string }>(
|
|
1591
|
+
vectors: T[],
|
|
1592
|
+
namespace?: string,
|
|
1593
|
+
): T[] => {
|
|
1738
1594
|
if (!namespace) return vectors;
|
|
1739
1595
|
return vectors.map((vector) => (vector.namespace ? vector : { ...vector, namespace }));
|
|
1740
1596
|
};
|
|
@@ -1752,7 +1608,10 @@ export function buildFunctionVectorizeProxy(
|
|
|
1752
1608
|
if (directBinding) {
|
|
1753
1609
|
switch (body.action) {
|
|
1754
1610
|
case 'upsert': {
|
|
1755
|
-
const vectors = withNamespace(
|
|
1611
|
+
const vectors = withNamespace(
|
|
1612
|
+
body.vectors as VectorizeVector[],
|
|
1613
|
+
body.namespace as string | undefined,
|
|
1614
|
+
);
|
|
1756
1615
|
let count = 0;
|
|
1757
1616
|
let mutationId: string | undefined;
|
|
1758
1617
|
for (const chunk of chunkArray(vectors, VECTOR_BATCH_LIMIT)) {
|
|
@@ -1765,7 +1624,10 @@ export function buildFunctionVectorizeProxy(
|
|
|
1765
1624
|
return { count, ...(mutationId ? { mutationId } : {}) };
|
|
1766
1625
|
}
|
|
1767
1626
|
case 'insert': {
|
|
1768
|
-
const vectors = withNamespace(
|
|
1627
|
+
const vectors = withNamespace(
|
|
1628
|
+
body.vectors as VectorizeVector[],
|
|
1629
|
+
body.namespace as string | undefined,
|
|
1630
|
+
);
|
|
1769
1631
|
let count = 0;
|
|
1770
1632
|
let mutationId: string | undefined;
|
|
1771
1633
|
for (const chunk of chunkArray(vectors, VECTOR_BATCH_LIMIT)) {
|
|
@@ -1788,9 +1650,11 @@ export function buildFunctionVectorizeProxy(
|
|
|
1788
1650
|
return { matches: mapMatches(result.matches), count: result.count };
|
|
1789
1651
|
}
|
|
1790
1652
|
case 'queryById': {
|
|
1791
|
-
const queryById = (
|
|
1792
|
-
|
|
1793
|
-
|
|
1653
|
+
const queryById = (
|
|
1654
|
+
directBinding as unknown as {
|
|
1655
|
+
queryById?: (id: string, opts?: VectorizeQueryOptions) => Promise<VectorizeMatches>;
|
|
1656
|
+
}
|
|
1657
|
+
).queryById;
|
|
1794
1658
|
if (typeof queryById !== 'function') {
|
|
1795
1659
|
throw new Error('queryById is not available on this Vectorize binding');
|
|
1796
1660
|
}
|
|
@@ -1805,7 +1669,11 @@ export function buildFunctionVectorizeProxy(
|
|
|
1805
1669
|
}
|
|
1806
1670
|
case 'getByIds': {
|
|
1807
1671
|
const vectors = (
|
|
1808
|
-
await Promise.all(
|
|
1672
|
+
await Promise.all(
|
|
1673
|
+
chunkArray(body.ids as string[], VECTOR_BATCH_LIMIT).map((chunk) =>
|
|
1674
|
+
directBinding.getByIds(chunk),
|
|
1675
|
+
),
|
|
1676
|
+
)
|
|
1809
1677
|
).flat();
|
|
1810
1678
|
return {
|
|
1811
1679
|
vectors: vectors.map((vector) => ({
|
|
@@ -1833,12 +1701,22 @@ export function buildFunctionVectorizeProxy(
|
|
|
1833
1701
|
const details = info as unknown as Record<string, unknown>;
|
|
1834
1702
|
return {
|
|
1835
1703
|
vectorCount: details.vectorCount ?? details.vectorsCount ?? 0,
|
|
1836
|
-
dimensions:
|
|
1837
|
-
|
|
1704
|
+
dimensions:
|
|
1705
|
+
details.dimensions ??
|
|
1706
|
+
(details.config as Record<string, unknown> | undefined)?.dimensions ??
|
|
1707
|
+
0,
|
|
1708
|
+
metric:
|
|
1709
|
+
details.metric ??
|
|
1710
|
+
(details.config as Record<string, unknown> | undefined)?.metric ??
|
|
1711
|
+
'cosine',
|
|
1838
1712
|
...('id' in details ? { id: details.id } : {}),
|
|
1839
1713
|
...('name' in details ? { name: details.name } : {}),
|
|
1840
|
-
...('processedUpToDatetime' in details
|
|
1841
|
-
|
|
1714
|
+
...('processedUpToDatetime' in details
|
|
1715
|
+
? { processedUpToDatetime: details.processedUpToDatetime }
|
|
1716
|
+
: {}),
|
|
1717
|
+
...('processedUpToMutation' in details
|
|
1718
|
+
? { processedUpToMutation: details.processedUpToMutation }
|
|
1719
|
+
: {}),
|
|
1842
1720
|
};
|
|
1843
1721
|
}
|
|
1844
1722
|
}
|