@edge-base/server 0.2.1 → 0.2.2
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/{BY07qVPA.js → 4vlsb8ej.js} +1 -1
- package/admin-build/_app/immutable/chunks/{D755Tqat.js → 5PDcRlfX.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BFs_qStz.js → B8DT4fss.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DnLqc9L1.js → BEYYl662.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Dqk2TGNU.js → BKXmgPq4.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DjOEv9M9.js → BWyDPAjM.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BcIUK2sk.js → BaCHY17I.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B0QyxC2M.js → C-DsDCNG.js} +3 -3
- package/admin-build/_app/immutable/chunks/{k0CIJkw4.js → C85dMlzL.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BsFiK_FJ.js → CPdXvRUb.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BCKr7yKd.js → CTngeX8H.js} +1 -1
- package/admin-build/_app/immutable/chunks/{D-x55wdW.js → DzXaj-Ja.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BTJcQFEp.js → c5iKSdWY.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CqUxCvs_.js → g3ZZdY-r.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CSGrwS7E.js → kiJ6KthZ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{m9QZTyVV.js → qiZXAKh-.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.BTsq3_xq.js → app.BZxfavhF.js} +2 -2
- package/admin-build/_app/immutable/entry/start.Mr9mmopc.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.BZ00WDYH.js → 0.DlsaydXO.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.RzSJ3yyr.js → 1.D2NWN5eG.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.D-rsiquF.js → 10.EMDaN3nw.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.l7-bgtFD.js → 11.BasqQ_o9.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.Dkq0H7B5.js → 12.DO31Ljs7.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.DtK_4oRz.js → 13.DhyAy-GZ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.BKo7-AMx.js → 14.CLecGWc4.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.CQAj_6lq.js → 15.B9kp3W4e.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.XVIG-Ffr.js → 16.Pu_8T3RI.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.g6raZLCM.js → 17.DX4z43t6.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.IQz6a3T6.js → 18.BKsSaxrr.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.CAAZ8i8h.js → 19.DXNF1htN.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.BPcX3KPj.js → 20.VRVb0wee.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.Ck3_0D2f.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.Br5AG_5Z.js → 22.DqZf4CtH.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.KjbrdXoE.js → 23.DtyxMiQG.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.C3n2-hgw.js → 24.CloWNmTd.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.SFDSBzHd.js → 25.CnZWMq7_.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.D95vui6E.js → 26.DrV7XOmf.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.FgLgdjwB.js → 27.DV8L32OF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.B9sYYm1F.js → 28.Stil2D4u.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.DyqZ_wbN.js → 29.Zsm1e5Dc.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.Bzo2yVIO.js → 3.CKoj2vNz.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.c1CiNwiS.js → 30.Ni0k5bER.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.CXty66Vh.js → 31.mnqj9EbV.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.BgQaXZ27.js → 4.B_-z9AzT.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.BuJrHvxH.js → 5.yiZ72j4k.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.CkBBC94k.js → 6.BqykybBG.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.D2YBvNFM.js → 7.BDAHlhsF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.D8qQWo_z.js → 8.D8Xvy0lH.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.BLDLX5hV.js → 9.Dddmd7_F.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__/functions-context.test.ts +5 -5
- package/src/__tests__/meta-export-coverage.test.ts +1 -0
- package/src/lib/functions.ts +204 -397
- package/src/lib/internal-transport.ts +316 -0
- package/src/lib/plugin-migrations.ts +2 -2
- package/src/routes/admin.ts +7 -1
- package/src/routes/auth.ts +6 -12
- package/src/routes/storage.ts +6 -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 {
|
|
28
|
+
import { getD1BindingName, shouldRouteToD1 } from './do-router.js';
|
|
29
29
|
import { executeDoSql } from './do-sql.js';
|
|
30
30
|
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';
|
|
31
|
+
import { ensureAuthSchema } from './auth-d1.js';
|
|
34
32
|
import type { Env } from '../types.js';
|
|
35
33
|
import { createSignedToken } from '../routes/storage.js';
|
|
36
34
|
import {
|
|
35
|
+
createManagedAdminUser,
|
|
37
36
|
deleteManagedAdminUser,
|
|
38
37
|
normalizeAdminUserUpdates,
|
|
39
38
|
updateManagedAdminUser,
|
|
40
39
|
} from './admin-user-management.js';
|
|
40
|
+
import { hashPassword } from './password.js';
|
|
41
|
+
import { generateId } from './uuid.js';
|
|
42
|
+
import { DbRef, TableRef, DefaultDbApi, HttpClient, ContextManager } from '@edge-base/core';
|
|
43
|
+
import { InternalHttpTransport } from './internal-transport.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,31 @@ 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 with direct D1/DO binding access — no HTTP round-trip.
|
|
99
|
+
* Routes directly to D1 binding or Durable Object without network overhead.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* const rows = await ctx.admin.sqlWithDirectD1Access('shared', undefined, 'SELECT * FROM posts WHERE status = ?', ['published']);
|
|
103
|
+
*/
|
|
104
|
+
sqlWithDirectD1Access(
|
|
108
105
|
namespace: string,
|
|
109
106
|
id: string | undefined,
|
|
110
107
|
query: string,
|
|
@@ -291,20 +288,21 @@ export interface FunctionContext {
|
|
|
291
288
|
*
|
|
292
289
|
* @example
|
|
293
290
|
* // Static DB
|
|
294
|
-
* await context.db('shared').table('posts').
|
|
291
|
+
* await context.db('shared').table('posts').getList()
|
|
295
292
|
* // Dynamic DB
|
|
296
|
-
* await context.db('workspace', 'ws-456').table('documents').
|
|
293
|
+
* await context.db('workspace', 'ws-456').table('documents').getList()
|
|
294
|
+
* // With query builder
|
|
295
|
+
* await context.db('shared').table('posts').where('status', '==', 'published').limit(10).getList()
|
|
297
296
|
*/
|
|
298
297
|
db: FunctionAdminContext['db'];
|
|
299
298
|
/**
|
|
300
|
-
* Server-side EdgeBase admin client (§5
|
|
299
|
+
* Server-side EdgeBase admin client (§5).
|
|
301
300
|
* Use context.admin.db(namespace, id?).table(name) for all DB access.
|
|
301
|
+
* Uses the same TableRef from @edge-base/core as the client SDK.
|
|
302
302
|
*
|
|
303
303
|
* @example
|
|
304
|
-
*
|
|
305
|
-
* await context.admin.db('shared').table('posts').
|
|
306
|
-
* // Dynamic DB
|
|
307
|
-
* await context.admin.db('workspace', 'ws-456').table('documents').list()
|
|
304
|
+
* await context.admin.db('shared').table('posts').getList()
|
|
305
|
+
* await context.admin.db('shared').table('posts').where('userId', '==', uid).getList()
|
|
308
306
|
*/
|
|
309
307
|
admin: FunctionAdminContext;
|
|
310
308
|
/**
|
|
@@ -697,314 +695,143 @@ export function wrapMethodExport(
|
|
|
697
695
|
};
|
|
698
696
|
}
|
|
699
697
|
|
|
700
|
-
// ───
|
|
698
|
+
// ─── Admin DB Proxy (uses @edge-base/core TableRef via InternalHttpTransport) ───
|
|
701
699
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
-
};
|
|
700
|
+
interface BuildAdminDbProxyOptions {
|
|
701
|
+
databaseNamespace: DurableObjectNamespace;
|
|
702
|
+
config: EdgeBaseConfig;
|
|
703
|
+
workerUrl?: string;
|
|
704
|
+
serviceKey?: string;
|
|
705
|
+
env?: Env;
|
|
706
|
+
executionCtx?: ExecutionContext;
|
|
707
|
+
preferDirectDo?: boolean;
|
|
708
|
+
}
|
|
758
709
|
|
|
759
|
-
|
|
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
|
-
}
|
|
710
|
+
// ─── Shared SQL executor — D1 direct → DO direct → HTTP fallback ───
|
|
784
711
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
},
|
|
793
|
-
body: body === undefined || method === 'GET' || method === 'DELETE'
|
|
794
|
-
? undefined
|
|
795
|
-
: JSON.stringify(body),
|
|
796
|
-
},
|
|
797
|
-
);
|
|
712
|
+
export interface SqlWithDirectD1AccessOptions {
|
|
713
|
+
env?: unknown;
|
|
714
|
+
config: EdgeBaseConfig;
|
|
715
|
+
databaseNamespace?: DurableObjectNamespace;
|
|
716
|
+
workerUrl?: string;
|
|
717
|
+
serviceKey?: string;
|
|
718
|
+
}
|
|
798
719
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
if (!
|
|
818
|
-
throw new Error(
|
|
720
|
+
/**
|
|
721
|
+
* Execute raw SQL with the fastest available path:
|
|
722
|
+
* 1. D1 direct binding (no network hop)
|
|
723
|
+
* 2. Durable Object direct call
|
|
724
|
+
* 3. HTTP fallback via workerUrl
|
|
725
|
+
*
|
|
726
|
+
* Shared by buildFunctionContext, auth hooks, and storage hooks.
|
|
727
|
+
*/
|
|
728
|
+
export async function executeSqlWithDirectD1Access(
|
|
729
|
+
opts: SqlWithDirectD1AccessOptions,
|
|
730
|
+
namespace: string,
|
|
731
|
+
id: string | undefined,
|
|
732
|
+
query: string,
|
|
733
|
+
params?: unknown[],
|
|
734
|
+
): Promise<unknown[]> {
|
|
735
|
+
if (opts.env) {
|
|
736
|
+
const dbBlock = opts.config.databases?.[namespace];
|
|
737
|
+
const isDynamicNamespace = !!(dbBlock?.instance || dbBlock?.access?.canCreate || dbBlock?.access?.access);
|
|
738
|
+
if (isDynamicNamespace && !id) {
|
|
739
|
+
throw new Error(`admin.sqlWithDirectD1Access() requires an id for dynamic namespace '${namespace}'.`);
|
|
819
740
|
}
|
|
820
741
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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;
|
|
742
|
+
if (!id && shouldRouteToD1(namespace, opts.config)) {
|
|
743
|
+
const bindingName = getD1BindingName(namespace);
|
|
744
|
+
const d1 = (opts.env as Record<string, unknown>)[bindingName] as D1Database | undefined;
|
|
745
|
+
if (!d1) {
|
|
746
|
+
throw new Error(`D1 binding '${bindingName}' not found.`);
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
const stmt = d1.prepare(query);
|
|
750
|
+
const bound = params && params.length > 0 ? stmt.bind(...params) : stmt;
|
|
751
|
+
const result = await bound.all();
|
|
752
|
+
return (result.results ?? []) as unknown[];
|
|
753
|
+
} catch (error) {
|
|
754
|
+
const message = error instanceof Error ? error.message : 'SQL execution failed';
|
|
755
|
+
throw new Error(message);
|
|
756
|
+
}
|
|
860
757
|
}
|
|
861
758
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
759
|
+
if (opts.databaseNamespace) {
|
|
760
|
+
return executeDoSql({
|
|
761
|
+
databaseNamespace: opts.databaseNamespace,
|
|
762
|
+
namespace,
|
|
763
|
+
id,
|
|
764
|
+
query,
|
|
765
|
+
params: params ?? [],
|
|
766
|
+
internal: true,
|
|
767
|
+
});
|
|
867
768
|
}
|
|
769
|
+
}
|
|
868
770
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
771
|
+
if (opts.workerUrl && opts.serviceKey) {
|
|
772
|
+
const res = await fetch(`${opts.workerUrl}/api/sql`, {
|
|
773
|
+
method: 'POST',
|
|
872
774
|
headers: {
|
|
873
|
-
|
|
874
|
-
'X-
|
|
775
|
+
'Content-Type': 'application/json',
|
|
776
|
+
'X-EdgeBase-Service-Key': opts.serviceKey,
|
|
875
777
|
},
|
|
778
|
+
body: JSON.stringify({ namespace, id, sql: query, params: params ?? [] }),
|
|
876
779
|
});
|
|
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
780
|
if (!res.ok) {
|
|
906
|
-
const err = (await res.json().catch(() => ({}))) as
|
|
907
|
-
throw new Error(
|
|
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}`);
|
|
781
|
+
const err = (await res.json().catch(() => ({ message: 'SQL execution failed' }))) as { message: string };
|
|
782
|
+
throw new Error(err.message);
|
|
972
783
|
}
|
|
973
|
-
|
|
974
|
-
|
|
784
|
+
const data = (await res.json()) as { rows?: unknown[]; items?: unknown[]; results?: unknown[] };
|
|
785
|
+
if (Array.isArray(data.rows)) return data.rows;
|
|
786
|
+
if (Array.isArray(data.items)) return data.items;
|
|
787
|
+
if (Array.isArray(data.results)) return data.results;
|
|
788
|
+
return [];
|
|
789
|
+
}
|
|
975
790
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
interface BuildAdminDbProxyOptions {
|
|
980
|
-
databaseNamespace: DurableObjectNamespace;
|
|
981
|
-
config: EdgeBaseConfig;
|
|
982
|
-
workerUrl?: string;
|
|
983
|
-
serviceKey?: string;
|
|
984
|
-
env?: Env;
|
|
985
|
-
executionCtx?: ExecutionContext;
|
|
986
|
-
preferDirectDo?: boolean;
|
|
791
|
+
throw new Error(
|
|
792
|
+
'admin.sqlWithDirectD1Access() requires env or workerUrl.',
|
|
793
|
+
);
|
|
987
794
|
}
|
|
988
795
|
|
|
796
|
+
/**
|
|
797
|
+
* Build the admin DB proxy that returns real DbRef/TableRef instances
|
|
798
|
+
* from @edge-base/core, routed through InternalHttpTransport for
|
|
799
|
+
* direct D1/PG/DO access (no HTTP round-trip).
|
|
800
|
+
*/
|
|
989
801
|
export function buildAdminDbProxy(options: BuildAdminDbProxyOptions): FunctionAdminContext['db'] {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
802
|
+
// Create HttpClient for sql() tagged template support on TableRef.
|
|
803
|
+
// Only available when workerUrl is set (routes through /api/sql endpoint).
|
|
804
|
+
let httpClient: HttpClient | undefined;
|
|
805
|
+
if (options.workerUrl) {
|
|
806
|
+
httpClient = new HttpClient({
|
|
807
|
+
baseUrl: options.workerUrl,
|
|
808
|
+
serviceKey: options.serviceKey,
|
|
809
|
+
contextManager: new ContextManager(),
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return (namespace: string, id?: string): DbRef => {
|
|
814
|
+
// Create a per-DbRef transport with explicit dbContext so that
|
|
815
|
+
// path parsing is unambiguous even when instanceId === 'tables'.
|
|
816
|
+
const transport = new InternalHttpTransport({
|
|
817
|
+
databaseNamespace: options.databaseNamespace,
|
|
818
|
+
config: options.config,
|
|
819
|
+
workerUrl: options.workerUrl,
|
|
820
|
+
serviceKey: options.serviceKey,
|
|
821
|
+
env: options.env,
|
|
822
|
+
executionCtx: options.executionCtx,
|
|
823
|
+
preferDirectDo: options.preferDirectDo,
|
|
824
|
+
dbContext: { namespace, instanceId: id },
|
|
825
|
+
});
|
|
826
|
+
const dbApi = new DefaultDbApi(transport);
|
|
827
|
+
return new DbRef(
|
|
828
|
+
dbApi,
|
|
829
|
+
namespace,
|
|
830
|
+
id,
|
|
831
|
+
undefined, // databaseLiveClient — not available server-side
|
|
832
|
+
undefined, // filterMatchFn
|
|
833
|
+
httpClient, // enables table().sql`...` tagged template
|
|
834
|
+
);
|
|
1008
835
|
};
|
|
1009
836
|
}
|
|
1010
837
|
|
|
@@ -1024,7 +851,7 @@ interface AdminAuthOptions {
|
|
|
1024
851
|
/**
|
|
1025
852
|
* Build admin auth context for App Functions.
|
|
1026
853
|
* Uses AUTH_DB D1 directly for all operations (D1-first architecture).
|
|
1027
|
-
* Cross-shard operations (listUsers
|
|
854
|
+
* Cross-shard operations (listUsers) also available via Worker HTTP relay
|
|
1028
855
|
* when workerUrl is provided.
|
|
1029
856
|
*/
|
|
1030
857
|
export function buildAdminAuthContext(options: AdminAuthOptions): AdminAuthContext {
|
|
@@ -1089,10 +916,49 @@ export function buildAdminAuthContext(options: AdminAuthOptions): AdminAuthConte
|
|
|
1089
916
|
displayName?: string;
|
|
1090
917
|
role?: string;
|
|
1091
918
|
}): Promise<Record<string, unknown>> {
|
|
919
|
+
// Direct D1 path — works without service key (same as updateUser/deleteUser)
|
|
920
|
+
if (d1Database) {
|
|
921
|
+
// Input validation (mirrors routes/admin-auth.ts guards)
|
|
922
|
+
if (!data.email || typeof data.email !== 'string') throw new Error('Email and password are required.');
|
|
923
|
+
if (!data.password || typeof data.password !== 'string') throw new Error('Email and password are required.');
|
|
924
|
+
const email = data.email.trim().toLowerCase();
|
|
925
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
926
|
+
throw new Error('Invalid email format.');
|
|
927
|
+
}
|
|
928
|
+
if (data.password.length < 8) throw new Error('Password must be at least 8 characters.');
|
|
929
|
+
if (data.password.length > 256) throw new Error('Password must not exceed 256 characters.');
|
|
930
|
+
if (data.displayName && data.displayName.length > 200) {
|
|
931
|
+
throw new Error('Display name must not exceed 200 characters.');
|
|
932
|
+
}
|
|
933
|
+
// Role validation (mirrors normalizeOptionalRole in routes/admin-auth.ts)
|
|
934
|
+
let role = 'user';
|
|
935
|
+
if (data.role !== undefined) {
|
|
936
|
+
if (typeof data.role !== 'string') throw new Error('Role must be a non-empty string.');
|
|
937
|
+
const trimmed = data.role.trim();
|
|
938
|
+
if (!trimmed) throw new Error('Role must be a non-empty string.');
|
|
939
|
+
if (trimmed.length > 100) throw new Error('Role must not exceed 100 characters.');
|
|
940
|
+
role = trimmed;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const db = new D1AuthDb(d1Database);
|
|
944
|
+
// Ensure auth tables exist (critical for fresh databases)
|
|
945
|
+
await ensureAuthSchema(db);
|
|
946
|
+
const user = await createManagedAdminUser(
|
|
947
|
+
db,
|
|
948
|
+
{
|
|
949
|
+
userId: generateId(),
|
|
950
|
+
email,
|
|
951
|
+
passwordHash: await hashPassword(data.password),
|
|
952
|
+
displayName: data.displayName,
|
|
953
|
+
role,
|
|
954
|
+
verified: true,
|
|
955
|
+
},
|
|
956
|
+
{ kv: kvNamespace },
|
|
957
|
+
);
|
|
958
|
+
return authService.sanitizeUser(user, { includeAppMetadata: true });
|
|
959
|
+
}
|
|
1092
960
|
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.
|
|
961
|
+
// HTTP relay fallback: POST /api/auth/admin/users → Worker → D1
|
|
1096
962
|
const res = await fetch(`${workerUrl}/api/auth/admin/users`, {
|
|
1097
963
|
method: 'POST',
|
|
1098
964
|
headers: {
|
|
@@ -1111,7 +977,7 @@ export function buildAdminAuthContext(options: AdminAuthOptions): AdminAuthConte
|
|
|
1111
977
|
return result.user;
|
|
1112
978
|
}
|
|
1113
979
|
throw new Error(
|
|
1114
|
-
'admin.auth.createUser() is not available in this context (requires workerUrl). ' +
|
|
980
|
+
'admin.auth.createUser() is not available in this context (requires D1 or workerUrl). ' +
|
|
1115
981
|
'Pass workerUrl to buildFunctionContext(), or use the external SDK.',
|
|
1116
982
|
);
|
|
1117
983
|
},
|
|
@@ -1223,77 +1089,18 @@ export function buildFunctionContext(options: BuildFunctionContextOptions): Func
|
|
|
1223
1089
|
// ─── context.admin.db(namespace, id) — DB-first tenant access (§5) ───
|
|
1224
1090
|
db: adminDb,
|
|
1225
1091
|
auth: adminAuthContext,
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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({
|
|
1092
|
+
// ─── Direct D1/DO SQL — delegates to shared executor ───
|
|
1093
|
+
sqlWithDirectD1Access: (namespace: string, id: string | undefined, query: string, params?: unknown[]) =>
|
|
1094
|
+
executeSqlWithDirectD1Access(
|
|
1095
|
+
{
|
|
1096
|
+
env: options.env,
|
|
1097
|
+
config: options.config,
|
|
1258
1098
|
databaseNamespace: options.databaseNamespace,
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
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
|
-
},
|
|
1099
|
+
workerUrl: options.workerUrl,
|
|
1100
|
+
serviceKey: options.serviceKey,
|
|
1101
|
+
},
|
|
1102
|
+
namespace, id, query, params,
|
|
1103
|
+
),
|
|
1297
1104
|
async broadcast(
|
|
1298
1105
|
channel: string,
|
|
1299
1106
|
event: string,
|