@edge-base/server 0.2.2 → 0.2.4
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/{C85dMlzL.js → 5RQRbp5q.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B8DT4fss.js → BME_U9TJ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BaCHY17I.js → BYI6CUvd.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BWyDPAjM.js → BgDzp0i0.js} +1 -1
- package/admin-build/_app/immutable/chunks/{c5iKSdWY.js → BjWZuf8W.js} +1 -1
- package/admin-build/_app/immutable/chunks/{g3ZZdY-r.js → C6lpZLE2.js} +1 -1
- package/admin-build/_app/immutable/chunks/{C-DsDCNG.js → D5GswVnI.js} +3 -3
- package/admin-build/_app/immutable/chunks/DBsVqhuh.js +1 -0
- package/admin-build/_app/immutable/chunks/{BEYYl662.js → DYaCRWMA.js} +1 -1
- package/admin-build/_app/immutable/chunks/D__dwMuW.js +1 -0
- package/admin-build/_app/immutable/chunks/{4vlsb8ej.js → Dj-E9-FO.js} +1 -1
- package/admin-build/_app/immutable/chunks/{kiJ6KthZ.js → Dj0QUuOf.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BKXmgPq4.js → XQM1k9PM.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CTngeX8H.js → fYEKMQ-Z.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CPdXvRUb.js → g_-Kpxu3.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DzXaj-Ja.js → wCNueVYy.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.BZxfavhF.js → app.C8ylfBe6.js} +2 -2
- package/admin-build/_app/immutable/entry/start.CtsqDyfj.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.DlsaydXO.js → 0.CJJ6HZbp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.D2NWN5eG.js → 1.B4sI5cB4.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.EMDaN3nw.js → 10.D6hvCer6.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.BasqQ_o9.js → 11.Dx7b8aQ5.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.DO31Ljs7.js → 12.Bqmy5KIF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.DhyAy-GZ.js → 13.CC6KpXgS.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.CLecGWc4.js → 14.yCo1Ix8E.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.B9kp3W4e.js → 15.co0UfPlh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.Pu_8T3RI.js → 16.D0xkPUBW.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.DX4z43t6.js → 17.CebNqPeh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.BKsSaxrr.js → 18.JUoLOZxh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.DXNF1htN.js → 19.ND8kmQJe.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.VRVb0wee.js → 20.DYb-q3W8.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.cz3IN9Cc.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.DqZf4CtH.js → 22.UOzm8WYV.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.DtyxMiQG.js → 23.BLgq21om.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.CloWNmTd.js → 24.DN9usmUs.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.CnZWMq7_.js → 25.BddRfAyE.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.DrV7XOmf.js → 26.Dl6XHIeT.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.DV8L32OF.js → 27.D0iNwALG.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.Stil2D4u.js → 28.9dKQmdGi.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.Zsm1e5Dc.js → 29.wXzfJUXp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.CKoj2vNz.js → 3.z8ut3jS-.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.Ni0k5bER.js → 30.BtZETNsL.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.mnqj9EbV.js → 31.CYonj2Jh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.B_-z9AzT.js → 4.COtDPQ9b.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.yiZ72j4k.js → 5.CTRCeIhp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.BqykybBG.js → 6.ChHi3QkR.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.BDAHlhsF.js → 7.CCMtr6Ac.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.D8Xvy0lH.js → 8.DpWJ-X_-.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.Dddmd7_F.js → 9.DOkvfmir.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/package.json +3 -3
- package/src/__tests__/admin-data-routes.test.ts +29 -0
- package/src/__tests__/d1-live-broadcast-verification.test.ts +271 -0
- package/src/__tests__/database-do-route-validation.test.ts +105 -0
- package/src/__tests__/database-live-do.test.ts +50 -0
- package/src/__tests__/database-live-emitter.test.ts +116 -1
- package/src/__tests__/database-live-route.test.ts +82 -0
- package/src/__tests__/do-router.test.ts +116 -0
- package/src/__tests__/error-format.test.ts +63 -0
- package/src/__tests__/functions-context.test.ts +674 -33
- package/src/__tests__/functions-d1-proxy.test.ts +54 -0
- package/src/__tests__/plugin-migration-routing.test.ts +32 -0
- package/src/__tests__/postgres-field-ops-compat.test.ts +110 -0
- package/src/__tests__/provider-aware-sql.test.ts +163 -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__/scheduled.test.ts +55 -0
- package/src/__tests__/service-key-db-proxy.test.ts +122 -1
- package/src/__tests__/sql-route.test.ts +252 -75
- package/src/__tests__/table-hook-runtime.test.ts +137 -0
- package/src/durable-objects/database-do.ts +36 -45
- 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/index.ts +12 -6
- package/src/lib/admin-db-target.ts +30 -74
- package/src/lib/d1-handler.ts +55 -35
- package/src/lib/database-live-emitter.ts +57 -16
- package/src/lib/do-router.ts +135 -3
- package/src/lib/functions.ts +215 -143
- package/src/lib/internal-transport.ts +28 -12
- package/src/lib/plugin-migration-routing.ts +28 -0
- package/src/lib/plugin-migrations.ts +38 -38
- package/src/lib/postgres-handler.ts +51 -31
- package/src/lib/provider-aware-sql.ts +831 -0
- package/src/lib/table-hook-runtime.ts +62 -0
- package/src/routes/admin.ts +41 -41
- package/src/routes/auth.ts +7 -2
- package/src/routes/database-live.ts +110 -12
- package/src/routes/sql.ts +64 -84
- package/src/routes/storage.ts +7 -2
- package/src/routes/tables.ts +42 -29
- package/admin-build/_app/immutable/chunks/5PDcRlfX.js +0 -1
- package/admin-build/_app/immutable/chunks/qiZXAKh-.js +0 -1
- package/admin-build/_app/immutable/entry/start.Mr9mmopc.js +0 -1
- package/admin-build/_app/immutable/nodes/21.Ck3_0D2f.js +0 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { EdgeBaseConfig, HookCtx } from '@edge-base/shared';
|
|
2
|
+
import { ensureAuthSchema } from './auth-d1.js';
|
|
3
|
+
import { resolveAuthDb, type AuthDb } from './auth-db-adapter.js';
|
|
4
|
+
import { sendToDatabaseLiveDO } from './database-live-emitter.js';
|
|
5
|
+
import { createPushProvider } from './push-provider.js';
|
|
6
|
+
import { getDevicesForUser } from './push-token.js';
|
|
7
|
+
import type { Env } from '../types.js';
|
|
8
|
+
|
|
9
|
+
type PushTokenStore = KVNamespace | { kv: KVNamespace; authDb?: AuthDb | null };
|
|
10
|
+
|
|
11
|
+
async function resolvePushTokenStore(env: Env): Promise<PushTokenStore | null> {
|
|
12
|
+
if (!env.KV) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const authDb = resolveAuthDb(env as unknown as Record<string, unknown>);
|
|
18
|
+
await ensureAuthSchema(authDb);
|
|
19
|
+
return { kv: env.KV, authDb };
|
|
20
|
+
} catch {
|
|
21
|
+
return env.KV;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildTableHookRuntimeServices(
|
|
26
|
+
config: EdgeBaseConfig,
|
|
27
|
+
env: Env,
|
|
28
|
+
): Pick<HookCtx, 'databaseLive' | 'push'> {
|
|
29
|
+
return {
|
|
30
|
+
databaseLive: {
|
|
31
|
+
async broadcast(channel: string, event: string, data: unknown): Promise<void> {
|
|
32
|
+
await sendToDatabaseLiveDO(
|
|
33
|
+
env,
|
|
34
|
+
{ channel, event, payload: data ?? {} },
|
|
35
|
+
'/internal/broadcast',
|
|
36
|
+
);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
push: {
|
|
40
|
+
async send(userId: string, payload: { title?: string; body: string }): Promise<void> {
|
|
41
|
+
try {
|
|
42
|
+
const tokenStore = await resolvePushTokenStore(env);
|
|
43
|
+
if (!tokenStore) return;
|
|
44
|
+
|
|
45
|
+
const provider = createPushProvider(config.push, env);
|
|
46
|
+
if (!provider) return;
|
|
47
|
+
|
|
48
|
+
const devices = await getDevicesForUser(tokenStore, userId);
|
|
49
|
+
if (devices.length === 0) return;
|
|
50
|
+
|
|
51
|
+
await Promise.allSettled(
|
|
52
|
+
devices.map((device) =>
|
|
53
|
+
provider.send({ token: device.token, platform: device.platform, payload }),
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.warn('[EdgeBase] table hook push.send failed:', error);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
package/src/routes/admin.ts
CHANGED
|
@@ -23,7 +23,16 @@ import {
|
|
|
23
23
|
import { hashPassword, verifyPassword } from '../lib/password.js';
|
|
24
24
|
import { generateId } from '../lib/uuid.js';
|
|
25
25
|
import { validateKey, buildConstraintCtx, extractBearerToken, resolveServiceKeyCandidate } from '../lib/service-key.js';
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
formatDbTargetValidationIssue,
|
|
28
|
+
getD1BindingName,
|
|
29
|
+
getDbDoName,
|
|
30
|
+
isDynamicDbBlock,
|
|
31
|
+
normalizeDbInstanceId,
|
|
32
|
+
parseConfig,
|
|
33
|
+
resolveDbTarget,
|
|
34
|
+
shouldRouteToD1,
|
|
35
|
+
} from '../lib/do-router.js';
|
|
27
36
|
import { handleD1Request, d1BatchImport } from '../lib/d1-handler.js';
|
|
28
37
|
import { fetchDOWithRetry } from '../lib/do-retry.js';
|
|
29
38
|
import { dumpNamespaceTables } from '../lib/namespace-dump.js';
|
|
@@ -734,19 +743,6 @@ function getTableDO(env: Env, tableName: string, config: ReturnType<typeof parse
|
|
|
734
743
|
return { stub: env.DATABASE.get(env.DATABASE.idFromName(doName)), doName };
|
|
735
744
|
}
|
|
736
745
|
|
|
737
|
-
function isDynamicDbBlock(
|
|
738
|
-
dbBlock: {
|
|
739
|
-
instance?: boolean;
|
|
740
|
-
access?: {
|
|
741
|
-
canCreate?: unknown;
|
|
742
|
-
access?: unknown;
|
|
743
|
-
};
|
|
744
|
-
} | undefined,
|
|
745
|
-
): boolean {
|
|
746
|
-
if (!dbBlock) return false;
|
|
747
|
-
return !!(dbBlock.instance || dbBlock.access?.canCreate || dbBlock.access?.access);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
746
|
function getEffectiveDbProvider(namespace: string, config: ReturnType<typeof parseConfig>): 'do' | 'd1' | 'postgres' | 'neon' {
|
|
751
747
|
const dbBlock = config.databases?.[namespace];
|
|
752
748
|
if (!dbBlock) return 'do';
|
|
@@ -763,10 +759,7 @@ function getEffectiveDbProvider(namespace: string, config: ReturnType<typeof par
|
|
|
763
759
|
}
|
|
764
760
|
|
|
765
761
|
function getRequestedInstanceId(c: { req: { query: (name: string) => string | undefined } }): string | undefined {
|
|
766
|
-
|
|
767
|
-
if (!raw) return undefined;
|
|
768
|
-
const trimmed = raw.trim();
|
|
769
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
762
|
+
return normalizeDbInstanceId(c.req.query('instanceId'));
|
|
770
763
|
}
|
|
771
764
|
|
|
772
765
|
function validateAdminTableInstanceId(
|
|
@@ -774,28 +767,27 @@ function validateAdminTableInstanceId(
|
|
|
774
767
|
config: ReturnType<typeof parseConfig>,
|
|
775
768
|
instanceId: string | undefined,
|
|
776
769
|
): Response | null {
|
|
777
|
-
const
|
|
778
|
-
if (
|
|
779
|
-
if (dynamic) {
|
|
780
|
-
return new Response(
|
|
781
|
-
JSON.stringify({
|
|
782
|
-
code: 400,
|
|
783
|
-
message: `instanceId is required for dynamic namespace '${namespace}'`,
|
|
784
|
-
}),
|
|
785
|
-
{
|
|
786
|
-
status: 400,
|
|
787
|
-
headers: { 'Content-Type': 'application/json' },
|
|
788
|
-
},
|
|
789
|
-
);
|
|
790
|
-
}
|
|
770
|
+
const target = resolveDbTarget(config, namespace, instanceId);
|
|
771
|
+
if (target.ok) {
|
|
791
772
|
return null;
|
|
792
773
|
}
|
|
793
|
-
|
|
794
|
-
|
|
774
|
+
if (target.status !== 400) {
|
|
775
|
+
return new Response(
|
|
776
|
+
JSON.stringify({
|
|
777
|
+
code: target.status,
|
|
778
|
+
message: formatDbTargetValidationIssue(target.issue, namespace),
|
|
779
|
+
}),
|
|
780
|
+
{
|
|
781
|
+
status: target.status,
|
|
782
|
+
headers: { 'Content-Type': 'application/json' },
|
|
783
|
+
},
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
if (target.issue === 'instance_id_invalid') {
|
|
795
787
|
return new Response(
|
|
796
788
|
JSON.stringify({
|
|
797
789
|
code: 400,
|
|
798
|
-
message:
|
|
790
|
+
message: formatDbTargetValidationIssue(target.issue, namespace),
|
|
799
791
|
}),
|
|
800
792
|
{
|
|
801
793
|
status: 400,
|
|
@@ -803,8 +795,16 @@ function validateAdminTableInstanceId(
|
|
|
803
795
|
},
|
|
804
796
|
);
|
|
805
797
|
}
|
|
806
|
-
|
|
807
|
-
|
|
798
|
+
return new Response(
|
|
799
|
+
JSON.stringify({
|
|
800
|
+
code: 400,
|
|
801
|
+
message: formatDbTargetValidationIssue(target.issue, namespace),
|
|
802
|
+
}),
|
|
803
|
+
{
|
|
804
|
+
status: 400,
|
|
805
|
+
headers: { 'Content-Type': 'application/json' },
|
|
806
|
+
},
|
|
807
|
+
);
|
|
808
808
|
}
|
|
809
809
|
|
|
810
810
|
async function restoreAdminNamespaceTables(
|
|
@@ -974,7 +974,7 @@ api.openapi(adminGetTableRecords, async (c) => {
|
|
|
974
974
|
const name = c.req.param('name')!;
|
|
975
975
|
const config = parseConfig(c.env);
|
|
976
976
|
const namespace = findNamespaceForTable(name, config);
|
|
977
|
-
const instanceId =
|
|
977
|
+
const instanceId = getRequestedInstanceId(c);
|
|
978
978
|
const instanceError = validateAdminTableInstanceId(namespace, config, instanceId);
|
|
979
979
|
if (instanceError) return instanceError;
|
|
980
980
|
|
|
@@ -1027,7 +1027,7 @@ api.openapi(adminCreateTableRecord, async (c) => {
|
|
|
1027
1027
|
const name = c.req.param('name')!;
|
|
1028
1028
|
const config = parseConfig(c.env);
|
|
1029
1029
|
const namespace = findNamespaceForTable(name, config);
|
|
1030
|
-
const instanceId =
|
|
1030
|
+
const instanceId = getRequestedInstanceId(c);
|
|
1031
1031
|
const instanceError = validateAdminTableInstanceId(namespace, config, instanceId);
|
|
1032
1032
|
if (instanceError) return instanceError;
|
|
1033
1033
|
|
|
@@ -1081,7 +1081,7 @@ api.openapi(adminUpdateTableRecord, async (c) => {
|
|
|
1081
1081
|
const id = c.req.param('id')!;
|
|
1082
1082
|
const config = parseConfig(c.env);
|
|
1083
1083
|
const namespace = findNamespaceForTable(name, config);
|
|
1084
|
-
const instanceId =
|
|
1084
|
+
const instanceId = getRequestedInstanceId(c);
|
|
1085
1085
|
const instanceError = validateAdminTableInstanceId(namespace, config, instanceId);
|
|
1086
1086
|
if (instanceError) return instanceError;
|
|
1087
1087
|
|
|
@@ -1129,7 +1129,7 @@ api.openapi(adminDeleteTableRecord, async (c) => {
|
|
|
1129
1129
|
const id = c.req.param('id')!;
|
|
1130
1130
|
const config = parseConfig(c.env);
|
|
1131
1131
|
const namespace = findNamespaceForTable(name, config);
|
|
1132
|
-
const instanceId =
|
|
1132
|
+
const instanceId = getRequestedInstanceId(c);
|
|
1133
1133
|
const instanceError = validateAdminTableInstanceId(namespace, config, instanceId);
|
|
1134
1134
|
if (instanceError) return instanceError;
|
|
1135
1135
|
|
package/src/routes/auth.ts
CHANGED
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
buildFunctionPushProxy,
|
|
55
55
|
buildAdminAuthContext,
|
|
56
56
|
buildAdminDbProxy,
|
|
57
|
-
|
|
57
|
+
executeSqlProviderAware,
|
|
58
58
|
getWorkerUrl,
|
|
59
59
|
} from '../lib/functions.js';
|
|
60
60
|
import * as authService from '../lib/auth-d1-service.js';
|
|
@@ -660,8 +660,13 @@ export async function executeAuthHook(
|
|
|
660
660
|
db: adminDb,
|
|
661
661
|
table: (name: string) => adminDb('shared').table(name),
|
|
662
662
|
auth: authAdmin,
|
|
663
|
+
sqlProviderAware: (namespace: string, id: string | undefined, query: string, params?: unknown[]) =>
|
|
664
|
+
executeSqlProviderAware(
|
|
665
|
+
{ env, config, databaseNamespace: env.DATABASE, workerUrl: options.workerUrl, serviceKey },
|
|
666
|
+
namespace, id, query, params,
|
|
667
|
+
),
|
|
663
668
|
sqlWithDirectD1Access: (namespace: string, id: string | undefined, query: string, params?: unknown[]) =>
|
|
664
|
-
|
|
669
|
+
executeSqlProviderAware(
|
|
665
670
|
{ env, config, databaseNamespace: env.DATABASE, workerUrl: options.workerUrl, serviceKey },
|
|
666
671
|
namespace, id, query, params,
|
|
667
672
|
),
|
|
@@ -9,7 +9,12 @@ import {
|
|
|
9
9
|
DATABASE_LIVE_HUB_DO_NAME,
|
|
10
10
|
isDbLiveChannel,
|
|
11
11
|
} from '../lib/database-live-emitter.js';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
formatDbTargetValidationIssue,
|
|
14
|
+
isDynamicDbBlock,
|
|
15
|
+
parseConfig,
|
|
16
|
+
resolveDbTarget,
|
|
17
|
+
} from '../lib/do-router.js';
|
|
13
18
|
import { validateKey, buildConstraintCtx } from '../lib/service-key.js';
|
|
14
19
|
import { getTrustedClientIp } from '../lib/client-ip.js';
|
|
15
20
|
import {
|
|
@@ -50,18 +55,107 @@ const dbConnectDiagnosticSchema = z.object({
|
|
|
50
55
|
maxPending: z.number().optional(),
|
|
51
56
|
});
|
|
52
57
|
|
|
53
|
-
function
|
|
58
|
+
function resolveStructuredDatabaseLiveChannel(
|
|
59
|
+
config: ReturnType<typeof parseConfig>,
|
|
60
|
+
query: {
|
|
61
|
+
namespace?: string;
|
|
62
|
+
instanceId?: string;
|
|
63
|
+
table?: string;
|
|
64
|
+
docId?: string;
|
|
65
|
+
},
|
|
66
|
+
): { ok: true; channel: string } | { ok: false; message: string } {
|
|
67
|
+
if (!query.namespace || !query.table) {
|
|
68
|
+
return { ok: false, message: 'Database subscription target required' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const target = resolveDbTarget(config, query.namespace, query.instanceId);
|
|
72
|
+
if (!target.ok) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
message: formatDbTargetValidationIssue(target.issue, query.namespace),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (!target.value.dbBlock.tables?.[query.table]) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
message: `Table '${query.table}' not found in database '${query.namespace}'`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const channel = buildDbLiveChannel(
|
|
86
|
+
query.namespace,
|
|
87
|
+
query.table,
|
|
88
|
+
target.value.instanceId,
|
|
89
|
+
query.docId,
|
|
90
|
+
);
|
|
91
|
+
if (!isDbLiveChannel(channel)) {
|
|
92
|
+
return { ok: false, message: 'Invalid database subscription target' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { ok: true, channel };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolveDatabaseLiveChannel(
|
|
99
|
+
config: ReturnType<typeof parseConfig>,
|
|
100
|
+
query: {
|
|
54
101
|
channel?: string;
|
|
55
102
|
namespace?: string;
|
|
56
103
|
instanceId?: string;
|
|
57
104
|
table?: string;
|
|
58
105
|
docId?: string;
|
|
59
|
-
}): string |
|
|
106
|
+
}): { ok: true; channel: string } | { ok: false; message: string } {
|
|
60
107
|
if (query.channel) {
|
|
61
|
-
|
|
108
|
+
if (!isDbLiveChannel(query.channel)) {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
message: `Database live only supports DB channels: ${query.channel}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const parts = query.channel.split(':');
|
|
116
|
+
const namespace = parts[1];
|
|
117
|
+
if (!namespace) {
|
|
118
|
+
return { ok: false, message: 'Database subscription target required' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const dbBlock = config.databases?.[namespace];
|
|
122
|
+
if (!dbBlock) {
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
message: formatDbTargetValidationIssue('namespace_not_found', namespace),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const dynamic = isDynamicDbBlock(dbBlock);
|
|
130
|
+
if (dynamic && parts.length < 4) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
message: formatDbTargetValidationIssue('instance_id_required', namespace),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (!dynamic && parts.length > 4) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
message: formatDbTargetValidationIssue('instance_id_not_allowed', namespace),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const structured = dynamic
|
|
144
|
+
? {
|
|
145
|
+
namespace,
|
|
146
|
+
instanceId: parts[2],
|
|
147
|
+
table: parts[3],
|
|
148
|
+
docId: parts[4],
|
|
149
|
+
}
|
|
150
|
+
: {
|
|
151
|
+
namespace,
|
|
152
|
+
table: parts[2],
|
|
153
|
+
docId: parts[3],
|
|
154
|
+
};
|
|
155
|
+
return resolveStructuredDatabaseLiveChannel(config, structured);
|
|
62
156
|
}
|
|
63
|
-
|
|
64
|
-
return
|
|
157
|
+
|
|
158
|
+
return resolveStructuredDatabaseLiveChannel(config, query);
|
|
65
159
|
}
|
|
66
160
|
|
|
67
161
|
function getPendingKey(ip: string): string {
|
|
@@ -85,7 +179,8 @@ const checkDatabaseConnection = createRoute({
|
|
|
85
179
|
});
|
|
86
180
|
|
|
87
181
|
databaseLiveRoute.openapi(checkDatabaseConnection, async (c) => {
|
|
88
|
-
const
|
|
182
|
+
const config = parseConfig(c.env);
|
|
183
|
+
const channelResult = resolveDatabaseLiveChannel(config, {
|
|
89
184
|
channel: c.req.query('channel') ?? undefined,
|
|
90
185
|
namespace: c.req.query('namespace') ?? undefined,
|
|
91
186
|
instanceId: c.req.query('instanceId') ?? undefined,
|
|
@@ -93,14 +188,15 @@ databaseLiveRoute.openapi(checkDatabaseConnection, async (c) => {
|
|
|
93
188
|
docId: c.req.query('docId') ?? undefined,
|
|
94
189
|
});
|
|
95
190
|
|
|
96
|
-
if (!
|
|
191
|
+
if (!channelResult.ok) {
|
|
97
192
|
return c.json({
|
|
98
193
|
ok: false,
|
|
99
194
|
type: 'db_connect_invalid_request',
|
|
100
195
|
category: 'request',
|
|
101
|
-
message:
|
|
196
|
+
message: channelResult.message,
|
|
102
197
|
}, 400);
|
|
103
198
|
}
|
|
199
|
+
const channel = channelResult.channel;
|
|
104
200
|
|
|
105
201
|
const ip = getTrustedClientIp(c.env, c.req) ?? 'unknown';
|
|
106
202
|
const kvKey = getPendingKey(ip);
|
|
@@ -224,16 +320,18 @@ databaseLiveRoute.openapi(connectDatabaseSubscription, async (c) => {
|
|
|
224
320
|
return c.json({ code: 400, message: 'Expected WebSocket upgrade' }, 400);
|
|
225
321
|
}
|
|
226
322
|
|
|
227
|
-
const
|
|
323
|
+
const config = parseConfig(c.env);
|
|
324
|
+
const channelResult = resolveDatabaseLiveChannel(config, {
|
|
228
325
|
channel: c.req.query('channel') ?? undefined,
|
|
229
326
|
namespace: c.req.query('namespace') ?? undefined,
|
|
230
327
|
instanceId: c.req.query('instanceId') ?? undefined,
|
|
231
328
|
table: c.req.query('table') ?? undefined,
|
|
232
329
|
docId: c.req.query('docId') ?? undefined,
|
|
233
330
|
});
|
|
234
|
-
if (!
|
|
235
|
-
return c.json({ code: 400, message:
|
|
331
|
+
if (!channelResult.ok) {
|
|
332
|
+
return c.json({ code: 400, message: channelResult.message }, 400);
|
|
236
333
|
}
|
|
334
|
+
const channel = channelResult.channel;
|
|
237
335
|
|
|
238
336
|
const ip = getTrustedClientIp(c.env, c.req) ?? 'unknown';
|
|
239
337
|
const kvKey = getPendingKey(ip);
|
package/src/routes/sql.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SQL endpoint — POST /api/sql
|
|
3
3
|
*
|
|
4
|
-
* Allows server SDK (with Service Key) to execute raw SQL
|
|
4
|
+
* Allows server SDK (with Service Key) to execute provider-aware raw SQL.
|
|
5
5
|
* NOT available to client SDK (no sql() method on ClientEdgeBase).
|
|
6
6
|
*
|
|
7
7
|
* §11: URL stays /api/sql, but request body now uses
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - id, if provided, must not contain ':' (§2)
|
|
14
14
|
* - Parameterized queries enforced (sql + params separate)
|
|
15
15
|
*
|
|
16
|
-
* Flow: Server SDK → POST /api/sql → Worker →
|
|
16
|
+
* Flow: Server SDK → POST /api/sql → Worker → provider-aware executor → JSON
|
|
17
17
|
*
|
|
18
18
|
* Request body:
|
|
19
19
|
* { namespace: string, id?: string, sql: string, params?: unknown[] }
|
|
@@ -23,19 +23,19 @@
|
|
|
23
23
|
* { namespace: 'workspace', id: 'ws-456', sql: 'SELECT * FROM documents', params: [] }
|
|
24
24
|
*/
|
|
25
25
|
import { OpenAPIHono, createRoute, type HonoEnv } from '../lib/hono.js';
|
|
26
|
-
import {
|
|
27
|
-
|
|
26
|
+
import {
|
|
27
|
+
formatDbTargetValidationIssue,
|
|
28
|
+
parseConfig,
|
|
29
|
+
resolveDbTarget,
|
|
30
|
+
} from '../lib/do-router.js';
|
|
28
31
|
import { validateKey, buildConstraintCtx } from '../lib/service-key.js';
|
|
29
|
-
import { zodDefaultHook, sqlBodySchema, jsonResponseSchema, errorResponseSchema } from '../lib/schemas.js';
|
|
30
32
|
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} from '../lib/
|
|
36
|
-
import {
|
|
37
|
-
import { executeDoSql } from '../lib/do-sql.js';
|
|
38
|
-
|
|
33
|
+
zodDefaultHook,
|
|
34
|
+
sqlBodySchema,
|
|
35
|
+
jsonResponseSchema,
|
|
36
|
+
errorResponseSchema,
|
|
37
|
+
} from '../lib/schemas.js';
|
|
38
|
+
import { executeProviderAwareSql } from '../lib/provider-aware-sql.js';
|
|
39
39
|
|
|
40
40
|
export const sqlRoute = new OpenAPIHono<HonoEnv>({ defaultHook: zodDefaultHook });
|
|
41
41
|
|
|
@@ -48,15 +48,31 @@ const executeSql = createRoute({
|
|
|
48
48
|
method: 'post',
|
|
49
49
|
path: '/',
|
|
50
50
|
tags: ['admin'],
|
|
51
|
-
summary: 'Execute
|
|
51
|
+
summary: 'Execute provider-aware raw SQL',
|
|
52
52
|
request: {
|
|
53
53
|
body: { content: { 'application/json': { schema: sqlBodySchema } }, required: true },
|
|
54
54
|
},
|
|
55
55
|
responses: {
|
|
56
|
-
200: {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
200: {
|
|
57
|
+
description: 'Query results',
|
|
58
|
+
content: { 'application/json': { schema: jsonResponseSchema } },
|
|
59
|
+
},
|
|
60
|
+
400: {
|
|
61
|
+
description: 'Bad request',
|
|
62
|
+
content: { 'application/json': { schema: errorResponseSchema } },
|
|
63
|
+
},
|
|
64
|
+
401: {
|
|
65
|
+
description: 'Unauthorized',
|
|
66
|
+
content: { 'application/json': { schema: errorResponseSchema } },
|
|
67
|
+
},
|
|
68
|
+
403: {
|
|
69
|
+
description: 'Forbidden',
|
|
70
|
+
content: { 'application/json': { schema: errorResponseSchema } },
|
|
71
|
+
},
|
|
72
|
+
404: {
|
|
73
|
+
description: 'Namespace not found',
|
|
74
|
+
content: { 'application/json': { schema: errorResponseSchema } },
|
|
75
|
+
},
|
|
60
76
|
},
|
|
61
77
|
});
|
|
62
78
|
|
|
@@ -76,23 +92,27 @@ sqlRoute.openapi(executeSql, async (c) => {
|
|
|
76
92
|
if (id !== undefined && id !== null && typeof id !== 'string') {
|
|
77
93
|
return c.json({ code: 400, message: 'id must be a string' }, 400);
|
|
78
94
|
}
|
|
79
|
-
if (id && id.includes(':')) {
|
|
80
|
-
return c.json({ code: 400, message: 'id must not contain \':\' (§2)' }, 400);
|
|
81
|
-
}
|
|
82
95
|
if (!sql || typeof sql !== 'string') {
|
|
83
96
|
return c.json({ code: 400, message: 'sql is required' }, 400);
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
// Validate namespace is declared in databases config (§1)
|
|
87
100
|
const config = parseConfig(c.env);
|
|
88
|
-
const
|
|
89
|
-
if (!
|
|
90
|
-
return c.json(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
const target = resolveDbTarget(config, namespace, id);
|
|
102
|
+
if (!target.ok) {
|
|
103
|
+
return c.json(
|
|
104
|
+
{
|
|
105
|
+
code: target.status,
|
|
106
|
+
message: formatDbTargetValidationIssue(target.issue, namespace, {
|
|
107
|
+
namespaceLabel: 'Namespace',
|
|
108
|
+
instanceIdLabel: 'id',
|
|
109
|
+
includeSectionRef: target.issue === 'instance_id_invalid',
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
target.status,
|
|
113
|
+
);
|
|
95
114
|
}
|
|
115
|
+
const { instanceId } = target.value;
|
|
96
116
|
|
|
97
117
|
// Service Key required AND validated
|
|
98
118
|
const { result: skResult } = validateKey(
|
|
@@ -110,65 +130,25 @@ sqlRoute.openapi(executeSql, async (c) => {
|
|
|
110
130
|
return c.json({ code: 401, message: 'Unauthorized. Invalid Service Key.' }, 401);
|
|
111
131
|
}
|
|
112
132
|
|
|
113
|
-
if (!id && (dbBlock?.provider === 'neon' || dbBlock?.provider === 'postgres')) {
|
|
114
|
-
const bindingName = getProviderBindingName(namespace);
|
|
115
|
-
const envRecord = c.env as unknown as Record<string, unknown>;
|
|
116
|
-
const hyperdrive = envRecord[bindingName] as { connectionString?: string } | undefined;
|
|
117
|
-
const envKey = dbBlock.connectionString ?? `${bindingName}_URL`;
|
|
118
|
-
const connStr = hyperdrive?.connectionString ?? (envRecord[envKey] as string | undefined);
|
|
119
|
-
if (!connStr) {
|
|
120
|
-
return c.json({ code: 500, message: `PostgreSQL connection '${envKey}' not found.` }, 500);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
const localDevOptions = getLocalDevPostgresExecOptions(c.env as unknown as Record<string, unknown>, namespace);
|
|
125
|
-
if (localDevOptions) {
|
|
126
|
-
await ensureLocalDevPostgresSchema(localDevOptions);
|
|
127
|
-
}
|
|
128
|
-
const result = await withPostgresConnection(connStr, async (query) => {
|
|
129
|
-
if (!localDevOptions) {
|
|
130
|
-
await ensurePgSchema(connStr, namespace, dbBlock.tables ?? {}, query);
|
|
131
|
-
}
|
|
132
|
-
return query(sql, params ?? []);
|
|
133
|
-
}, localDevOptions);
|
|
134
|
-
const rows = result.rows ?? [];
|
|
135
|
-
return c.json({ rows, items: rows, results: rows, columns: result.columns, rowCount: result.rowCount });
|
|
136
|
-
} catch (err) {
|
|
137
|
-
const message = err instanceof Error ? err.message : 'SQL execution failed';
|
|
138
|
-
return c.json({ code: 500, message }, 500);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (!id && shouldRouteToD1(namespace, config)) {
|
|
143
|
-
const bindingName = getD1BindingName(namespace);
|
|
144
|
-
const d1 = (c.env as unknown as Record<string, unknown>)[bindingName] as D1Database | undefined;
|
|
145
|
-
if (!d1) {
|
|
146
|
-
return c.json({ code: 500, message: `D1 binding '${bindingName}' not found.` }, 500);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
const result = await executeD1Sql(d1, sql, params ?? []);
|
|
151
|
-
return c.json({
|
|
152
|
-
rows: result.rows,
|
|
153
|
-
items: result.rows,
|
|
154
|
-
results: result.rows,
|
|
155
|
-
rowCount: result.rowCount,
|
|
156
|
-
});
|
|
157
|
-
} catch (err) {
|
|
158
|
-
const message = err instanceof Error ? err.message : 'SQL execution failed';
|
|
159
|
-
return c.json({ code: 500, message }, 500);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
133
|
try {
|
|
164
|
-
const
|
|
165
|
-
|
|
134
|
+
const result = await executeProviderAwareSql(
|
|
135
|
+
{
|
|
136
|
+
env: c.env,
|
|
137
|
+
config,
|
|
138
|
+
databaseNamespace: c.env.DATABASE,
|
|
139
|
+
},
|
|
166
140
|
namespace,
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
params
|
|
141
|
+
instanceId,
|
|
142
|
+
sql,
|
|
143
|
+
params ?? [],
|
|
144
|
+
);
|
|
145
|
+
return c.json({
|
|
146
|
+
rows: result.rows,
|
|
147
|
+
items: result.rows,
|
|
148
|
+
results: result.rows,
|
|
149
|
+
columns: result.columns,
|
|
150
|
+
rowCount: result.rowCount,
|
|
170
151
|
});
|
|
171
|
-
return c.json({ rows, items: rows, results: rows });
|
|
172
152
|
} catch (err) {
|
|
173
153
|
const message = err instanceof Error ? err.message : 'SQL execution failed';
|
|
174
154
|
return c.json({ code: 500, message }, 500);
|
package/src/routes/storage.ts
CHANGED
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
buildFunctionPushProxy,
|
|
45
45
|
buildAdminAuthContext,
|
|
46
46
|
buildAdminDbProxy,
|
|
47
|
-
|
|
47
|
+
executeSqlProviderAware,
|
|
48
48
|
getWorkerUrl,
|
|
49
49
|
} from '../lib/functions.js';
|
|
50
50
|
|
|
@@ -166,8 +166,13 @@ function buildStorageHookAdminContext(
|
|
|
166
166
|
db: adminDb,
|
|
167
167
|
table: (name: string) => adminDb('shared').table(name),
|
|
168
168
|
auth: buildAdminAuthContext({ d1Database: env.AUTH_DB, serviceKey, workerUrl }),
|
|
169
|
+
sqlProviderAware: (namespace: string, id: string | undefined, query: string, params?: unknown[]) =>
|
|
170
|
+
executeSqlProviderAware(
|
|
171
|
+
{ env, config, databaseNamespace: env.DATABASE, workerUrl, serviceKey },
|
|
172
|
+
namespace, id, query, params,
|
|
173
|
+
),
|
|
169
174
|
sqlWithDirectD1Access: (namespace: string, id: string | undefined, query: string, params?: unknown[]) =>
|
|
170
|
-
|
|
175
|
+
executeSqlProviderAware(
|
|
171
176
|
{ env, config, databaseNamespace: env.DATABASE, workerUrl, serviceKey },
|
|
172
177
|
namespace, id, query, params,
|
|
173
178
|
),
|