@edge-base/server 0.1.1
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/.gitkeep +0 -0
- package/admin-build/_app/env.js +1 -0
- package/admin-build/_app/immutable/assets/0.Bm6cF078.css +1 -0
- package/admin-build/_app/immutable/assets/1.BfW3pUNa.css +1 -0
- package/admin-build/_app/immutable/assets/11.CVmQOewb.css +1 -0
- package/admin-build/_app/immutable/assets/12.B1EhbRZT.css +1 -0
- package/admin-build/_app/immutable/assets/13.BvwYeuwE.css +1 -0
- package/admin-build/_app/immutable/assets/14.CdVfcO0R.css +1 -0
- package/admin-build/_app/immutable/assets/15.2yeZ66b-.css +1 -0
- package/admin-build/_app/immutable/assets/17.BVg0JEVu.css +1 -0
- package/admin-build/_app/immutable/assets/18.Rwnl3x_i.css +1 -0
- package/admin-build/_app/immutable/assets/20.DsPWA9AV.css +1 -0
- package/admin-build/_app/immutable/assets/21.Dz2RJ56c.css +1 -0
- package/admin-build/_app/immutable/assets/22.DwNLk5Ai.css +1 -0
- package/admin-build/_app/immutable/assets/23.CFpu0gOO.css +1 -0
- package/admin-build/_app/immutable/assets/24.Cy5LBeoJ.css +1 -0
- package/admin-build/_app/immutable/assets/25.pUyLVf-h.css +1 -0
- package/admin-build/_app/immutable/assets/26.DBcGrlXa.css +1 -0
- package/admin-build/_app/immutable/assets/27.BswYyAJD.css +1 -0
- package/admin-build/_app/immutable/assets/28.B4ueB1Kf.css +1 -0
- package/admin-build/_app/immutable/assets/29.B-qU6PdF.css +1 -0
- package/admin-build/_app/immutable/assets/3.Dg81Pgmd.css +1 -0
- package/admin-build/_app/immutable/assets/30.CsdWum94.css +1 -0
- package/admin-build/_app/immutable/assets/31.U6OwIp50.css +1 -0
- package/admin-build/_app/immutable/assets/4.CyawCCux.css +1 -0
- package/admin-build/_app/immutable/assets/5.C0YO2HTk.css +1 -0
- package/admin-build/_app/immutable/assets/8.Br5jd6kD.css +1 -0
- package/admin-build/_app/immutable/assets/Badge.EMYLHBxE.css +1 -0
- package/admin-build/_app/immutable/assets/Button.DpzMRTjK.css +1 -0
- package/admin-build/_app/immutable/assets/ConfirmDialog.DAnaWRRk.css +1 -0
- package/admin-build/_app/immutable/assets/EmptyState.CwKsu57Y.css +1 -0
- package/admin-build/_app/immutable/assets/Input.BDUSenmU.css +1 -0
- package/admin-build/_app/immutable/assets/Modal.Dm5B0Xie.css +1 -0
- package/admin-build/_app/immutable/assets/PageShell.CmU-Xh-b.css +1 -0
- package/admin-build/_app/immutable/assets/SchemaFieldEditor.g4NsCdno.css +1 -0
- package/admin-build/_app/immutable/assets/Select.BW4Keufm.css +1 -0
- package/admin-build/_app/immutable/assets/Skeleton.KWUulTKJ.css +1 -0
- package/admin-build/_app/immutable/assets/Tabs.CniGYb67.css +1 -0
- package/admin-build/_app/immutable/assets/TimeChart.BTCDAvmT.css +1 -0
- package/admin-build/_app/immutable/assets/Toggle.Cy_K12OM.css +1 -0
- package/admin-build/_app/immutable/assets/TopList.ClFzmPlA.css +1 -0
- package/admin-build/_app/immutable/chunks/7B47DvSx.js +1 -0
- package/admin-build/_app/immutable/chunks/7f08Id8e.js +1 -0
- package/admin-build/_app/immutable/chunks/8wJeQ7LN.js +1 -0
- package/admin-build/_app/immutable/chunks/B-h2afW5.js +1 -0
- package/admin-build/_app/immutable/chunks/B8vJP3wz.js +1 -0
- package/admin-build/_app/immutable/chunks/BR_fL5Yv.js +1 -0
- package/admin-build/_app/immutable/chunks/BY92tFS2.js +1 -0
- package/admin-build/_app/immutable/chunks/BcR-Rdj9.js +1 -0
- package/admin-build/_app/immutable/chunks/BdrwyZv8.js +1 -0
- package/admin-build/_app/immutable/chunks/Bh56EfQ_.js +1 -0
- package/admin-build/_app/immutable/chunks/BkrCkgYp.js +1 -0
- package/admin-build/_app/immutable/chunks/BmRjiP5k.js +1 -0
- package/admin-build/_app/immutable/chunks/BsokvhWC.js +1 -0
- package/admin-build/_app/immutable/chunks/C4D51vTW.js +1 -0
- package/admin-build/_app/immutable/chunks/C6puvcoR.js +2 -0
- package/admin-build/_app/immutable/chunks/CCKNu7m7.js +1 -0
- package/admin-build/_app/immutable/chunks/CWj6FrbW.js +1 -0
- package/admin-build/_app/immutable/chunks/Ce-ngf4p.js +5 -0
- package/admin-build/_app/immutable/chunks/Cs0GwzJA.js +1 -0
- package/admin-build/_app/immutable/chunks/CwROoZK0.js +1 -0
- package/admin-build/_app/immutable/chunks/CxCPv_Ut.js +1 -0
- package/admin-build/_app/immutable/chunks/CxbRue-5.js +1 -0
- package/admin-build/_app/immutable/chunks/CyqB6g-D.js +1 -0
- package/admin-build/_app/immutable/chunks/D5h5A1cc.js +2 -0
- package/admin-build/_app/immutable/chunks/DnyL7Zq-.js +1 -0
- package/admin-build/_app/immutable/chunks/DoPXzH7F.js +1 -0
- package/admin-build/_app/immutable/chunks/DrQSgw-f.js +1 -0
- package/admin-build/_app/immutable/chunks/DttM2zNO.js +1 -0
- package/admin-build/_app/immutable/chunks/DuXuUBWN.js +1 -0
- package/admin-build/_app/immutable/chunks/MdeqaOQx.js +10 -0
- package/admin-build/_app/immutable/chunks/NuUjtcO2.js +1 -0
- package/admin-build/_app/immutable/chunks/Q2nPFxS6.js +1 -0
- package/admin-build/_app/immutable/chunks/R6arueIl.js +1 -0
- package/admin-build/_app/immutable/chunks/UUazaC_N.js +1 -0
- package/admin-build/_app/immutable/chunks/cOYbrQxx.js +1 -0
- package/admin-build/_app/immutable/chunks/eFQHTGwA.js +1 -0
- package/admin-build/_app/immutable/chunks/ehbppgYb.js +1 -0
- package/admin-build/_app/immutable/chunks/glwixJlP.js +1 -0
- package/admin-build/_app/immutable/chunks/vApWTCBs.js +1 -0
- package/admin-build/_app/immutable/chunks/w89G9Xpi.js +1 -0
- package/admin-build/_app/immutable/chunks/wJsUhbfZ.js +1 -0
- package/admin-build/_app/immutable/chunks/zfauFM8P.js +1 -0
- package/admin-build/_app/immutable/entry/app.CcO-Uos3.js +2 -0
- package/admin-build/_app/immutable/entry/start.COebYq3I.js +1 -0
- package/admin-build/_app/immutable/nodes/0.CjtHKU-6.js +1 -0
- package/admin-build/_app/immutable/nodes/1.DEisjlM0.js +1 -0
- package/admin-build/_app/immutable/nodes/10.CvhdyWVB.js +1 -0
- package/admin-build/_app/immutable/nodes/11.DjHqcOvy.js +1 -0
- package/admin-build/_app/immutable/nodes/12.mQLz4Mj_.js +1 -0
- package/admin-build/_app/immutable/nodes/13.CBonZZyP.js +110 -0
- package/admin-build/_app/immutable/nodes/14.d-oiZL0j.js +3 -0
- package/admin-build/_app/immutable/nodes/15.CKPQsUYF.js +1 -0
- package/admin-build/_app/immutable/nodes/16.wPzAPQGx.js +1 -0
- package/admin-build/_app/immutable/nodes/17.DayhKyEZ.js +1 -0
- package/admin-build/_app/immutable/nodes/18.DKwS0Ir0.js +1 -0
- package/admin-build/_app/immutable/nodes/19.wPzAPQGx.js +1 -0
- package/admin-build/_app/immutable/nodes/2.BKoKrw1i.js +1 -0
- package/admin-build/_app/immutable/nodes/20.BvIkkkrW.js +1 -0
- package/admin-build/_app/immutable/nodes/21.DMaFhdHk.js +128 -0
- package/admin-build/_app/immutable/nodes/22.3xdgwuK1.js +1 -0
- package/admin-build/_app/immutable/nodes/23.8Bvgjbsl.js +112 -0
- package/admin-build/_app/immutable/nodes/24.DzSSzRhG.js +2 -0
- package/admin-build/_app/immutable/nodes/25.9KKYBnAE.js +2 -0
- package/admin-build/_app/immutable/nodes/26.Bhn9dfhY.js +1 -0
- package/admin-build/_app/immutable/nodes/27.kRLiC24G.js +1 -0
- package/admin-build/_app/immutable/nodes/28.BVIN1-7N.js +1 -0
- package/admin-build/_app/immutable/nodes/29.3yabZWj4.js +1 -0
- package/admin-build/_app/immutable/nodes/3.BFtSOkX7.js +2 -0
- package/admin-build/_app/immutable/nodes/30.CyCQlwaP.js +1 -0
- package/admin-build/_app/immutable/nodes/31.C4LDXjES.js +1 -0
- package/admin-build/_app/immutable/nodes/4.CvbiMlCa.js +1 -0
- package/admin-build/_app/immutable/nodes/5.C6BLv2eM.js +1 -0
- package/admin-build/_app/immutable/nodes/6.BcXvfl2P.js +1 -0
- package/admin-build/_app/immutable/nodes/7.CIuqhPiK.js +1 -0
- package/admin-build/_app/immutable/nodes/8.BQOR_JfO.js +1 -0
- package/admin-build/_app/immutable/nodes/9.NZqXQxPy.js +1 -0
- package/admin-build/_app/version.json +1 -0
- package/admin-build/favicon.svg +26 -0
- package/admin-build/index.html +45 -0
- package/openapi.json +19543 -0
- package/package.json +66 -0
- package/src/__tests__/admin-assets.test.ts +55 -0
- package/src/__tests__/admin-data-routes.test.ts +488 -0
- package/src/__tests__/admin-db-target.test.ts +103 -0
- package/src/__tests__/admin-routing.test.ts +31 -0
- package/src/__tests__/admin-user-management.test.ts +311 -0
- package/src/__tests__/analytics-query.test.ts +75 -0
- package/src/__tests__/auth-d1.test.ts +749 -0
- package/src/__tests__/auth-db-adapter.test.ts +73 -0
- package/src/__tests__/auth-jwt.test.ts +440 -0
- package/src/__tests__/auth-oauth.test.ts +389 -0
- package/src/__tests__/auth-password.test.ts +367 -0
- package/src/__tests__/auth-redirect.test.ts +87 -0
- package/src/__tests__/backup-restore.test.ts +711 -0
- package/src/__tests__/broadcast.test.ts +128 -0
- package/src/__tests__/cli.test.ts +178 -0
- package/src/__tests__/cloudflare-realtime.test.ts +113 -0
- package/src/__tests__/config.test.ts +469 -0
- package/src/__tests__/cors.test.ts +154 -0
- package/src/__tests__/cron.test.ts +302 -0
- package/src/__tests__/d1-handler.test.ts +402 -0
- package/src/__tests__/d1-sql.test.ts +120 -0
- package/src/__tests__/database-live-config.test.ts +42 -0
- package/src/__tests__/database-live-emitter.test.ts +56 -0
- package/src/__tests__/database-live-filters.test.ts +63 -0
- package/src/__tests__/database-live-route.test.ts +113 -0
- package/src/__tests__/db-sql.test.ts +163 -0
- package/src/__tests__/do-lifecycle.test.ts +263 -0
- package/src/__tests__/do-router.test.ts +729 -0
- package/src/__tests__/email-provider.test.ts +128 -0
- package/src/__tests__/email-templates.test.ts +528 -0
- package/src/__tests__/error-format.test.ts +250 -0
- package/src/__tests__/field-ops.test.ts +242 -0
- package/src/__tests__/functions-context.test.ts +334 -0
- package/src/__tests__/functions-d1-proxy.test.ts +229 -0
- package/src/__tests__/functions-registry-runtime-config.test.ts +17 -0
- package/src/__tests__/functions-route.test.ts +139 -0
- package/src/__tests__/internal-request.test.ts +77 -0
- package/src/__tests__/log-writer.test.ts +44 -0
- package/src/__tests__/logger.test.ts +58 -0
- package/src/__tests__/meta-admin-proxy.test.ts +48 -0
- package/src/__tests__/meta-export-coverage.test.ts +191 -0
- package/src/__tests__/meta-route-registration.test.ts +47 -0
- package/src/__tests__/namespace-dump.test.ts +28 -0
- package/src/__tests__/oauth-providers.test.ts +337 -0
- package/src/__tests__/openapi-coverage.test.ts +144 -0
- package/src/__tests__/pagination.test.ts +59 -0
- package/src/__tests__/password-policy.test.ts +191 -0
- package/src/__tests__/plugin-migrations.test.ts +379 -0
- package/src/__tests__/postgres-batch-compat.test.ts +133 -0
- package/src/__tests__/postgres-dialect.test.ts +328 -0
- package/src/__tests__/postgres-executor.test.ts +79 -0
- package/src/__tests__/postgres-field-ops-compat.test.ts +222 -0
- package/src/__tests__/postgres-schema-init.test.ts +105 -0
- package/src/__tests__/postgres-table-utils.test.ts +107 -0
- package/src/__tests__/presence.test.ts +199 -0
- package/src/__tests__/provider.test.ts +550 -0
- package/src/__tests__/public-user-profile.test.ts +339 -0
- package/src/__tests__/push-handlers.test.ts +179 -0
- package/src/__tests__/push-provider.test.ts +80 -0
- package/src/__tests__/push-token.test.ts +418 -0
- package/src/__tests__/query.test.ts +771 -0
- package/src/__tests__/rate-limit.test.ts +260 -0
- package/src/__tests__/room-access-policy.test.ts +101 -0
- package/src/__tests__/room-handler-context.test.ts +130 -0
- package/src/__tests__/room-monitoring.test.ts +138 -0
- package/src/__tests__/room-runtime-routing.test.ts +222 -0
- package/src/__tests__/room.test.ts +254 -0
- package/src/__tests__/route-parser.test.ts +490 -0
- package/src/__tests__/rules.test.ts +234 -0
- package/src/__tests__/runtime-surface-accounting.test.ts +120 -0
- package/src/__tests__/scheduled.test.ts +80 -0
- package/src/__tests__/schema.test.ts +1273 -0
- package/src/__tests__/security-hardening.test.ts +312 -0
- package/src/__tests__/server.unit.test.ts +333 -0
- package/src/__tests__/service-key-db-proxy.test.ts +650 -0
- package/src/__tests__/service-key-provider-bypass.test.ts +138 -0
- package/src/__tests__/service-key.test.ts +757 -0
- package/src/__tests__/smoke-skip-report.test.ts +72 -0
- package/src/__tests__/sms-provider.test.ts +39 -0
- package/src/__tests__/sql-route.test.ts +218 -0
- package/src/__tests__/storage-hook-context.test.ts +115 -0
- package/src/__tests__/totp.test.ts +200 -0
- package/src/__tests__/uuid.test.ts +144 -0
- package/src/__tests__/validation.test.ts +773 -0
- package/src/__tests__/websocket-pending.test.ts +163 -0
- package/src/_functions-registry.ts +51 -0
- package/src/bench-entry.ts +9 -0
- package/src/cloudflare-test.d.ts +1 -0
- package/src/durable-objects/auth-do.ts +49 -0
- package/src/durable-objects/database-do.ts +2240 -0
- package/src/durable-objects/database-live-do.ts +949 -0
- package/src/durable-objects/logs-do.ts +1200 -0
- package/src/durable-objects/room-runtime-base.ts +1604 -0
- package/src/durable-objects/rooms-do.ts +2191 -0
- package/src/generated-config.ts +6 -0
- package/src/index.ts +382 -0
- package/src/lib/admin-assets.ts +54 -0
- package/src/lib/admin-db-target.ts +301 -0
- package/src/lib/admin-routing.ts +35 -0
- package/src/lib/admin-user-management.ts +464 -0
- package/src/lib/analytics-adapter.ts +103 -0
- package/src/lib/analytics-query.ts +579 -0
- package/src/lib/auth-d1-service.ts +1193 -0
- package/src/lib/auth-d1.ts +1056 -0
- package/src/lib/auth-db-adapter.ts +289 -0
- package/src/lib/auth-redirect.ts +116 -0
- package/src/lib/cidr.ts +115 -0
- package/src/lib/client-ip.ts +51 -0
- package/src/lib/cloudflare-realtime.ts +251 -0
- package/src/lib/control-db.ts +36 -0
- package/src/lib/cron.ts +163 -0
- package/src/lib/d1-handler.ts +1425 -0
- package/src/lib/d1-schema-init.ts +255 -0
- package/src/lib/d1-sql.ts +33 -0
- package/src/lib/database-live-config.ts +24 -0
- package/src/lib/database-live-emitter.ts +111 -0
- package/src/lib/db-sql.ts +66 -0
- package/src/lib/do-retry.ts +36 -0
- package/src/lib/do-router.ts +270 -0
- package/src/lib/do-sql.ts +73 -0
- package/src/lib/email-provider.ts +379 -0
- package/src/lib/email-templates.ts +285 -0
- package/src/lib/email-translations.ts +422 -0
- package/src/lib/errors.ts +151 -0
- package/src/lib/functions.ts +2091 -0
- package/src/lib/hono.ts +56 -0
- package/src/lib/internal-request.ts +56 -0
- package/src/lib/jwt.ts +354 -0
- package/src/lib/log-writer.ts +272 -0
- package/src/lib/namespace-dump.ts +125 -0
- package/src/lib/oauth-providers.ts +1225 -0
- package/src/lib/op-parser.ts +99 -0
- package/src/lib/openapi.ts +146 -0
- package/src/lib/pagination.ts +19 -0
- package/src/lib/password-policy.ts +102 -0
- package/src/lib/password.ts +145 -0
- package/src/lib/plugin-migrations.ts +612 -0
- package/src/lib/postgres-executor.ts +203 -0
- package/src/lib/postgres-handler.ts +1102 -0
- package/src/lib/postgres-schema-init.ts +341 -0
- package/src/lib/postgres-table-utils.ts +87 -0
- package/src/lib/public-user-profile.ts +187 -0
- package/src/lib/push-provider.ts +409 -0
- package/src/lib/push-token.ts +294 -0
- package/src/lib/query-engine.ts +768 -0
- package/src/lib/room-monitoring.ts +97 -0
- package/src/lib/room-runtime.ts +14 -0
- package/src/lib/route-parser.ts +434 -0
- package/src/lib/schema.ts +538 -0
- package/src/lib/schemas.ts +152 -0
- package/src/lib/service-key.ts +419 -0
- package/src/lib/sms-provider.ts +230 -0
- package/src/lib/startup-config.ts +99 -0
- package/src/lib/totp.ts +242 -0
- package/src/lib/uuid.ts +87 -0
- package/src/lib/validation.ts +205 -0
- package/src/lib/version.ts +2 -0
- package/src/lib/websocket-pending.ts +40 -0
- package/src/middleware/auth.ts +169 -0
- package/src/middleware/captcha-verify.ts +217 -0
- package/src/middleware/cors.ts +159 -0
- package/src/middleware/error-handler.ts +54 -0
- package/src/middleware/internal-guard.ts +26 -0
- package/src/middleware/logger.ts +126 -0
- package/src/middleware/rate-limit.ts +283 -0
- package/src/middleware/rules.ts +475 -0
- package/src/routes/admin-auth.ts +447 -0
- package/src/routes/admin.ts +3501 -0
- package/src/routes/analytics-api.ts +290 -0
- package/src/routes/auth.ts +4222 -0
- package/src/routes/backup.ts +1466 -0
- package/src/routes/config.ts +53 -0
- package/src/routes/d1.ts +109 -0
- package/src/routes/database-live.ts +281 -0
- package/src/routes/functions.ts +155 -0
- package/src/routes/health.ts +32 -0
- package/src/routes/kv.ts +167 -0
- package/src/routes/oauth.ts +1055 -0
- package/src/routes/push.ts +1465 -0
- package/src/routes/room.ts +639 -0
- package/src/routes/schema-endpoint.ts +76 -0
- package/src/routes/sql.ts +176 -0
- package/src/routes/storage.ts +1674 -0
- package/src/routes/tables.ts +699 -0
- package/src/routes/users.ts +21 -0
- package/src/routes/vectorize.ts +372 -0
- package/src/types.ts +99 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-level database table routes — §7.
|
|
3
|
+
*
|
|
4
|
+
* REST URL patterns:
|
|
5
|
+
* Single-instance DB: /api/db/{namespace}/tables/{table}[/{id}][/action]
|
|
6
|
+
* Dynamic DB: /api/db/{namespace}/{instanceId}/tables/{table}[/{id}][/action]
|
|
7
|
+
*
|
|
8
|
+
* Worker acts as a pure proxy — all data operations happen inside DatabaseDOs.
|
|
9
|
+
* DO routing: getDbDoName(namespace, instanceId?) → 'shared' | 'workspace:ws-456' etc.
|
|
10
|
+
*
|
|
11
|
+
* All 18 routes (9 static + 9 dynamic) are explicitly defined via createRoute()
|
|
12
|
+
* for OpenAPI spec generation. SDK codegen reads these to generate type-safe
|
|
13
|
+
* client methods — NO hardcoded paths allowed in SDKs.
|
|
14
|
+
*
|
|
15
|
+
* 9 operations per DB type:
|
|
16
|
+
* GET list, get, count, search
|
|
17
|
+
* POST insert, batch, batchByFilter
|
|
18
|
+
* PATCH update
|
|
19
|
+
* DELETE delete
|
|
20
|
+
*
|
|
21
|
+
* Reference: /api/collections/* completely removed (§7). Now uses /api/db/*.
|
|
22
|
+
*/
|
|
23
|
+
import { OpenAPIHono, createRoute, z, type HonoEnv } from '../lib/hono.js';
|
|
24
|
+
import type { Context } from 'hono';
|
|
25
|
+
import { getDbDoName, parseConfig, shouldRouteToD1 } from '../lib/do-router.js';
|
|
26
|
+
import { fetchDOWithRetry } from '../lib/do-retry.js';
|
|
27
|
+
import {
|
|
28
|
+
queryParamsSchema, listResponseSchema, recordResponseSchema,
|
|
29
|
+
jsonResponseSchema, errorResponseSchema, zodDefaultHook,
|
|
30
|
+
} from '../lib/schemas.js';
|
|
31
|
+
import type { AuthContext } from '@edge-base/shared';
|
|
32
|
+
import { handlePgRequest } from '../lib/postgres-handler.js';
|
|
33
|
+
import { handleD1Request } from '../lib/d1-handler.js';
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
export const tablesRoute = new OpenAPIHono<HonoEnv>({ defaultHook: zodDefaultHook });
|
|
37
|
+
|
|
38
|
+
// ─── Shared param schemas ─────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const singleInstanceTableParams = z.object({
|
|
41
|
+
namespace: z.string().openapi({ description: 'Database namespace', example: 'app' }),
|
|
42
|
+
table: z.string().openapi({ description: 'Table name', example: 'posts' }),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const singleInstanceTableIdParams = z.object({
|
|
46
|
+
namespace: z.string().openapi({ description: 'Database namespace', example: 'app' }),
|
|
47
|
+
table: z.string().openapi({ description: 'Table name', example: 'posts' }),
|
|
48
|
+
id: z.string().openapi({ description: 'Record ID' }),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const dynamicTableParams = z.object({
|
|
52
|
+
namespace: z.string().openapi({ description: 'Database namespace', example: 'workspace' }),
|
|
53
|
+
instanceId: z.string().openapi({ description: 'Instance ID', example: 'ws-456' }),
|
|
54
|
+
table: z.string().openapi({ description: 'Table name', example: 'posts' }),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const dynamicTableIdParams = z.object({
|
|
58
|
+
namespace: z.string().openapi({ description: 'Database namespace', example: 'workspace' }),
|
|
59
|
+
instanceId: z.string().openapi({ description: 'Instance ID', example: 'ws-456' }),
|
|
60
|
+
table: z.string().openapi({ description: 'Table name', example: 'posts' }),
|
|
61
|
+
id: z.string().openapi({ description: 'Record ID' }),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** Query params for insert/batch — supports ?upsert=true&conflictTarget=email. */
|
|
65
|
+
const insertQuerySchema = z.object({
|
|
66
|
+
upsert: z.string().optional().openapi({ description: 'Set to "true" for upsert mode' }),
|
|
67
|
+
conflictTarget: z.string().optional().openapi({ description: 'Column to use for conflict detection in upsert mode' }),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ======================================================================
|
|
71
|
+
// SINGLE-INSTANCE DB: /{namespace}/tables/*
|
|
72
|
+
// Must be registered BEFORE dynamic /:namespace/:instanceId routes.
|
|
73
|
+
// Within GET routes: /count and /search BEFORE /{id} to avoid shadowing.
|
|
74
|
+
// ======================================================================
|
|
75
|
+
|
|
76
|
+
// ─── GET /{namespace}/tables/{table}/count ────────────────────────────
|
|
77
|
+
|
|
78
|
+
const dbSingleCountRecords = createRoute({
|
|
79
|
+
operationId: 'dbSingleCountRecords',
|
|
80
|
+
method: 'get',
|
|
81
|
+
path: '/{namespace}/tables/{table}/count',
|
|
82
|
+
tags: ['client'],
|
|
83
|
+
summary: 'Count records in a single-instance table',
|
|
84
|
+
request: {
|
|
85
|
+
params: singleInstanceTableParams,
|
|
86
|
+
query: queryParamsSchema,
|
|
87
|
+
},
|
|
88
|
+
responses: {
|
|
89
|
+
200: { description: 'Count result', content: { 'application/json': { schema: jsonResponseSchema } } },
|
|
90
|
+
400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
91
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
tablesRoute.openapi(dbSingleCountRecords, async (c) => {
|
|
96
|
+
const namespace = c.req.param('namespace')!;
|
|
97
|
+
const table = c.req.param('table')!;
|
|
98
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}/count`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ─── GET /{namespace}/tables/{table}/search ───────────────────────────
|
|
102
|
+
|
|
103
|
+
const dbSingleSearchRecords = createRoute({
|
|
104
|
+
operationId: 'dbSingleSearchRecords',
|
|
105
|
+
method: 'get',
|
|
106
|
+
path: '/{namespace}/tables/{table}/search',
|
|
107
|
+
tags: ['client'],
|
|
108
|
+
summary: 'Search records in a single-instance table',
|
|
109
|
+
request: {
|
|
110
|
+
params: singleInstanceTableParams,
|
|
111
|
+
query: queryParamsSchema,
|
|
112
|
+
},
|
|
113
|
+
responses: {
|
|
114
|
+
200: { description: 'Search results', content: { 'application/json': { schema: listResponseSchema } } },
|
|
115
|
+
400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
116
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
tablesRoute.openapi(dbSingleSearchRecords, async (c) => {
|
|
121
|
+
const namespace = c.req.param('namespace')!;
|
|
122
|
+
const table = c.req.param('table')!;
|
|
123
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}/search`);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ─── GET /{namespace}/tables/{table}/{id} ─────────────────────────────
|
|
127
|
+
|
|
128
|
+
const dbSingleGetRecord = createRoute({
|
|
129
|
+
operationId: 'dbSingleGetRecord',
|
|
130
|
+
method: 'get',
|
|
131
|
+
path: '/{namespace}/tables/{table}/{id}',
|
|
132
|
+
tags: ['client'],
|
|
133
|
+
summary: 'Get a single record from a single-instance table',
|
|
134
|
+
request: {
|
|
135
|
+
params: singleInstanceTableIdParams,
|
|
136
|
+
query: z.object({
|
|
137
|
+
fields: z.string().optional().openapi({ description: 'Comma-separated field names to return' }),
|
|
138
|
+
}),
|
|
139
|
+
},
|
|
140
|
+
responses: {
|
|
141
|
+
200: { description: 'Record found', content: { 'application/json': { schema: recordResponseSchema } } },
|
|
142
|
+
404: { description: 'Not found', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
143
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
tablesRoute.openapi(dbSingleGetRecord, async (c) => {
|
|
148
|
+
const namespace = c.req.param('namespace')!;
|
|
149
|
+
const table = c.req.param('table')!;
|
|
150
|
+
const id = c.req.param('id')!;
|
|
151
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}/${id}`);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ─── GET /{namespace}/tables/{table} ──────────────────────────────────
|
|
155
|
+
|
|
156
|
+
const dbSingleListRecords = createRoute({
|
|
157
|
+
operationId: 'dbSingleListRecords',
|
|
158
|
+
method: 'get',
|
|
159
|
+
path: '/{namespace}/tables/{table}',
|
|
160
|
+
tags: ['client'],
|
|
161
|
+
summary: 'List records from a single-instance table',
|
|
162
|
+
request: {
|
|
163
|
+
params: singleInstanceTableParams,
|
|
164
|
+
query: queryParamsSchema,
|
|
165
|
+
},
|
|
166
|
+
responses: {
|
|
167
|
+
200: { description: 'Success', content: { 'application/json': { schema: listResponseSchema } } },
|
|
168
|
+
400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
169
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
tablesRoute.openapi(dbSingleListRecords, async (c) => {
|
|
174
|
+
const namespace = c.req.param('namespace')!;
|
|
175
|
+
const table = c.req.param('table')!;
|
|
176
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}`);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ─── POST /{namespace}/tables/{table}/batch ───────────────────────────
|
|
180
|
+
|
|
181
|
+
const dbSingleBatchRecords = createRoute({
|
|
182
|
+
operationId: 'dbSingleBatchRecords',
|
|
183
|
+
method: 'post',
|
|
184
|
+
path: '/{namespace}/tables/{table}/batch',
|
|
185
|
+
tags: ['client'],
|
|
186
|
+
summary: 'Batch insert records into a single-instance table',
|
|
187
|
+
request: {
|
|
188
|
+
params: singleInstanceTableParams,
|
|
189
|
+
query: insertQuerySchema,
|
|
190
|
+
body: { content: { 'application/json': { schema: z.record(z.string(), z.unknown()) } }, required: true },
|
|
191
|
+
},
|
|
192
|
+
responses: {
|
|
193
|
+
200: { description: 'Batch result', content: { 'application/json': { schema: jsonResponseSchema } } },
|
|
194
|
+
400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
195
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
tablesRoute.openapi(dbSingleBatchRecords, async (c) => {
|
|
200
|
+
const namespace = c.req.param('namespace')!;
|
|
201
|
+
const table = c.req.param('table')!;
|
|
202
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}/batch`);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ─── POST /{namespace}/tables/{table}/batch-by-filter ─────────────────
|
|
206
|
+
|
|
207
|
+
const dbSingleBatchByFilter = createRoute({
|
|
208
|
+
operationId: 'dbSingleBatchByFilter',
|
|
209
|
+
method: 'post',
|
|
210
|
+
path: '/{namespace}/tables/{table}/batch-by-filter',
|
|
211
|
+
tags: ['client'],
|
|
212
|
+
summary: 'Batch update/delete records by filter in a single-instance table',
|
|
213
|
+
request: {
|
|
214
|
+
params: singleInstanceTableParams,
|
|
215
|
+
query: insertQuerySchema,
|
|
216
|
+
body: { content: { 'application/json': { schema: z.record(z.string(), z.unknown()) } }, required: true },
|
|
217
|
+
},
|
|
218
|
+
responses: {
|
|
219
|
+
200: { description: 'Batch result', content: { 'application/json': { schema: jsonResponseSchema } } },
|
|
220
|
+
400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
221
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
tablesRoute.openapi(dbSingleBatchByFilter, async (c) => {
|
|
226
|
+
const namespace = c.req.param('namespace')!;
|
|
227
|
+
const table = c.req.param('table')!;
|
|
228
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}/batch-by-filter`);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ─── POST /{namespace}/tables/{table} (insert) ────────────────────────
|
|
232
|
+
|
|
233
|
+
const dbSingleInsertRecord = createRoute({
|
|
234
|
+
operationId: 'dbSingleInsertRecord',
|
|
235
|
+
method: 'post',
|
|
236
|
+
path: '/{namespace}/tables/{table}',
|
|
237
|
+
tags: ['client'],
|
|
238
|
+
summary: 'Insert a record into a single-instance table',
|
|
239
|
+
request: {
|
|
240
|
+
params: singleInstanceTableParams,
|
|
241
|
+
query: insertQuerySchema,
|
|
242
|
+
body: { content: { 'application/json': { schema: z.record(z.string(), z.unknown()) } }, required: true },
|
|
243
|
+
},
|
|
244
|
+
responses: {
|
|
245
|
+
201: { description: 'Created', content: { 'application/json': { schema: recordResponseSchema } } },
|
|
246
|
+
200: { description: 'Updated (upsert)', content: { 'application/json': { schema: recordResponseSchema } } },
|
|
247
|
+
400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
248
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
tablesRoute.openapi(dbSingleInsertRecord, async (c) => {
|
|
253
|
+
const namespace = c.req.param('namespace')!;
|
|
254
|
+
const table = c.req.param('table')!;
|
|
255
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}`);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ─── PATCH /{namespace}/tables/{table}/{id} ───────────────────────────
|
|
259
|
+
|
|
260
|
+
const dbSingleUpdateRecord = createRoute({
|
|
261
|
+
operationId: 'dbSingleUpdateRecord',
|
|
262
|
+
method: 'patch',
|
|
263
|
+
path: '/{namespace}/tables/{table}/{id}',
|
|
264
|
+
tags: ['client'],
|
|
265
|
+
summary: 'Update a record in a single-instance table',
|
|
266
|
+
request: {
|
|
267
|
+
params: singleInstanceTableIdParams,
|
|
268
|
+
body: { content: { 'application/json': { schema: z.record(z.string(), z.unknown()) } }, required: true },
|
|
269
|
+
},
|
|
270
|
+
responses: {
|
|
271
|
+
200: { description: 'Updated', content: { 'application/json': { schema: recordResponseSchema } } },
|
|
272
|
+
404: { description: 'Not found', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
273
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
tablesRoute.openapi(dbSingleUpdateRecord, async (c) => {
|
|
278
|
+
const namespace = c.req.param('namespace')!;
|
|
279
|
+
const table = c.req.param('table')!;
|
|
280
|
+
const id = c.req.param('id')!;
|
|
281
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}/${id}`);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ─── DELETE /{namespace}/tables/{table}/{id} ──────────────────────────
|
|
285
|
+
|
|
286
|
+
const dbSingleDeleteRecord = createRoute({
|
|
287
|
+
operationId: 'dbSingleDeleteRecord',
|
|
288
|
+
method: 'delete',
|
|
289
|
+
path: '/{namespace}/tables/{table}/{id}',
|
|
290
|
+
tags: ['client'],
|
|
291
|
+
summary: 'Delete a record from a single-instance table',
|
|
292
|
+
request: {
|
|
293
|
+
params: singleInstanceTableIdParams,
|
|
294
|
+
},
|
|
295
|
+
responses: {
|
|
296
|
+
200: { description: 'Deleted', content: { 'application/json': { schema: jsonResponseSchema } } },
|
|
297
|
+
404: { description: 'Not found', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
298
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
tablesRoute.openapi(dbSingleDeleteRecord, async (c) => {
|
|
303
|
+
const namespace = c.req.param('namespace')!;
|
|
304
|
+
const table = c.req.param('table')!;
|
|
305
|
+
const id = c.req.param('id')!;
|
|
306
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}/${id}`);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ======================================================================
|
|
310
|
+
// DYNAMIC DB: /{namespace}/{instanceId}/tables/*
|
|
311
|
+
// Same 9 operations with additional namespace + instanceId params.
|
|
312
|
+
// ======================================================================
|
|
313
|
+
|
|
314
|
+
// ─── GET /{namespace}/{instanceId}/tables/{table}/count ───────────────
|
|
315
|
+
|
|
316
|
+
const dbCountRecords = createRoute({
|
|
317
|
+
operationId: 'dbCountRecords',
|
|
318
|
+
method: 'get',
|
|
319
|
+
path: '/{namespace}/{instanceId}/tables/{table}/count',
|
|
320
|
+
tags: ['client'],
|
|
321
|
+
summary: 'Count records in dynamic table',
|
|
322
|
+
request: {
|
|
323
|
+
params: dynamicTableParams,
|
|
324
|
+
query: queryParamsSchema,
|
|
325
|
+
},
|
|
326
|
+
responses: {
|
|
327
|
+
200: { description: 'Count result', content: { 'application/json': { schema: jsonResponseSchema } } },
|
|
328
|
+
400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
329
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
tablesRoute.openapi(dbCountRecords, async (c) => {
|
|
334
|
+
const namespace = c.req.param('namespace')!;
|
|
335
|
+
const instanceId = c.req.param('instanceId')!;
|
|
336
|
+
const table = c.req.param('table')!;
|
|
337
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}/count`);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// ─── GET /{namespace}/{instanceId}/tables/{table}/search ──────────────
|
|
341
|
+
|
|
342
|
+
const dbSearchRecords = createRoute({
|
|
343
|
+
operationId: 'dbSearchRecords',
|
|
344
|
+
method: 'get',
|
|
345
|
+
path: '/{namespace}/{instanceId}/tables/{table}/search',
|
|
346
|
+
tags: ['client'],
|
|
347
|
+
summary: 'Search records in dynamic table',
|
|
348
|
+
request: {
|
|
349
|
+
params: dynamicTableParams,
|
|
350
|
+
query: queryParamsSchema,
|
|
351
|
+
},
|
|
352
|
+
responses: {
|
|
353
|
+
200: { description: 'Search results', content: { 'application/json': { schema: listResponseSchema } } },
|
|
354
|
+
400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
355
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
tablesRoute.openapi(dbSearchRecords, async (c) => {
|
|
360
|
+
const namespace = c.req.param('namespace')!;
|
|
361
|
+
const instanceId = c.req.param('instanceId')!;
|
|
362
|
+
const table = c.req.param('table')!;
|
|
363
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}/search`);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// ─── GET /{namespace}/{instanceId}/tables/{table}/{id} ────────────────
|
|
367
|
+
|
|
368
|
+
const dbGetRecord = createRoute({
|
|
369
|
+
operationId: 'dbGetRecord',
|
|
370
|
+
method: 'get',
|
|
371
|
+
path: '/{namespace}/{instanceId}/tables/{table}/{id}',
|
|
372
|
+
tags: ['client'],
|
|
373
|
+
summary: 'Get single record from dynamic table',
|
|
374
|
+
request: {
|
|
375
|
+
params: dynamicTableIdParams,
|
|
376
|
+
query: z.object({
|
|
377
|
+
fields: z.string().optional().openapi({ description: 'Comma-separated field names to return' }),
|
|
378
|
+
}),
|
|
379
|
+
},
|
|
380
|
+
responses: {
|
|
381
|
+
200: { description: 'Record found', content: { 'application/json': { schema: recordResponseSchema } } },
|
|
382
|
+
404: { description: 'Not found', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
383
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
tablesRoute.openapi(dbGetRecord, async (c) => {
|
|
388
|
+
const namespace = c.req.param('namespace')!;
|
|
389
|
+
const instanceId = c.req.param('instanceId')!;
|
|
390
|
+
const table = c.req.param('table')!;
|
|
391
|
+
const id = c.req.param('id')!;
|
|
392
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}/${id}`);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// ─── GET /{namespace}/{instanceId}/tables/{table} ─────────────────────
|
|
396
|
+
|
|
397
|
+
const dbListRecords = createRoute({
|
|
398
|
+
operationId: 'dbListRecords',
|
|
399
|
+
method: 'get',
|
|
400
|
+
path: '/{namespace}/{instanceId}/tables/{table}',
|
|
401
|
+
tags: ['client'],
|
|
402
|
+
summary: 'List records from dynamic table',
|
|
403
|
+
request: {
|
|
404
|
+
params: dynamicTableParams,
|
|
405
|
+
query: queryParamsSchema,
|
|
406
|
+
},
|
|
407
|
+
responses: {
|
|
408
|
+
200: { description: 'Success', content: { 'application/json': { schema: listResponseSchema } } },
|
|
409
|
+
400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
410
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
tablesRoute.openapi(dbListRecords, async (c) => {
|
|
415
|
+
const namespace = c.req.param('namespace')!;
|
|
416
|
+
const instanceId = c.req.param('instanceId')!;
|
|
417
|
+
const table = c.req.param('table')!;
|
|
418
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}`);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ─── POST /{namespace}/{instanceId}/tables/{table}/batch ──────────────
|
|
422
|
+
|
|
423
|
+
const dbBatchRecords = createRoute({
|
|
424
|
+
operationId: 'dbBatchRecords',
|
|
425
|
+
method: 'post',
|
|
426
|
+
path: '/{namespace}/{instanceId}/tables/{table}/batch',
|
|
427
|
+
tags: ['client'],
|
|
428
|
+
summary: 'Batch insert records into dynamic table',
|
|
429
|
+
request: {
|
|
430
|
+
params: dynamicTableParams,
|
|
431
|
+
query: insertQuerySchema,
|
|
432
|
+
body: { content: { 'application/json': { schema: z.record(z.string(), z.unknown()) } }, required: true },
|
|
433
|
+
},
|
|
434
|
+
responses: {
|
|
435
|
+
200: { description: 'Batch result', content: { 'application/json': { schema: jsonResponseSchema } } },
|
|
436
|
+
400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
437
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
tablesRoute.openapi(dbBatchRecords, async (c) => {
|
|
442
|
+
const namespace = c.req.param('namespace')!;
|
|
443
|
+
const instanceId = c.req.param('instanceId')!;
|
|
444
|
+
const table = c.req.param('table')!;
|
|
445
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}/batch`);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// ─── POST /{namespace}/{instanceId}/tables/{table}/batch-by-filter ────
|
|
449
|
+
|
|
450
|
+
const dbBatchByFilter = createRoute({
|
|
451
|
+
operationId: 'dbBatchByFilter',
|
|
452
|
+
method: 'post',
|
|
453
|
+
path: '/{namespace}/{instanceId}/tables/{table}/batch-by-filter',
|
|
454
|
+
tags: ['client'],
|
|
455
|
+
summary: 'Batch update/delete records by filter in dynamic table',
|
|
456
|
+
request: {
|
|
457
|
+
params: dynamicTableParams,
|
|
458
|
+
query: insertQuerySchema,
|
|
459
|
+
body: { content: { 'application/json': { schema: z.record(z.string(), z.unknown()) } }, required: true },
|
|
460
|
+
},
|
|
461
|
+
responses: {
|
|
462
|
+
200: { description: 'Batch result', content: { 'application/json': { schema: jsonResponseSchema } } },
|
|
463
|
+
400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
464
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
tablesRoute.openapi(dbBatchByFilter, async (c) => {
|
|
469
|
+
const namespace = c.req.param('namespace')!;
|
|
470
|
+
const instanceId = c.req.param('instanceId')!;
|
|
471
|
+
const table = c.req.param('table')!;
|
|
472
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}/batch-by-filter`);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// ─── POST /{namespace}/{instanceId}/tables/{table} (insert) ───────────
|
|
476
|
+
|
|
477
|
+
const dbInsertRecord = createRoute({
|
|
478
|
+
operationId: 'dbInsertRecord',
|
|
479
|
+
method: 'post',
|
|
480
|
+
path: '/{namespace}/{instanceId}/tables/{table}',
|
|
481
|
+
tags: ['client'],
|
|
482
|
+
summary: 'Insert record into dynamic table',
|
|
483
|
+
request: {
|
|
484
|
+
params: dynamicTableParams,
|
|
485
|
+
query: insertQuerySchema,
|
|
486
|
+
body: { content: { 'application/json': { schema: z.record(z.string(), z.unknown()) } }, required: true },
|
|
487
|
+
},
|
|
488
|
+
responses: {
|
|
489
|
+
201: { description: 'Created', content: { 'application/json': { schema: recordResponseSchema } } },
|
|
490
|
+
200: { description: 'Updated (upsert)', content: { 'application/json': { schema: recordResponseSchema } } },
|
|
491
|
+
400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
492
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
tablesRoute.openapi(dbInsertRecord, async (c) => {
|
|
497
|
+
const namespace = c.req.param('namespace')!;
|
|
498
|
+
const instanceId = c.req.param('instanceId')!;
|
|
499
|
+
const table = c.req.param('table')!;
|
|
500
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}`);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// ─── PATCH /{namespace}/{instanceId}/tables/{table}/{id} ──────────────
|
|
504
|
+
|
|
505
|
+
const dbUpdateRecord = createRoute({
|
|
506
|
+
operationId: 'dbUpdateRecord',
|
|
507
|
+
method: 'patch',
|
|
508
|
+
path: '/{namespace}/{instanceId}/tables/{table}/{id}',
|
|
509
|
+
tags: ['client'],
|
|
510
|
+
summary: 'Update record in dynamic table',
|
|
511
|
+
request: {
|
|
512
|
+
params: dynamicTableIdParams,
|
|
513
|
+
body: { content: { 'application/json': { schema: z.record(z.string(), z.unknown()) } }, required: true },
|
|
514
|
+
},
|
|
515
|
+
responses: {
|
|
516
|
+
200: { description: 'Updated', content: { 'application/json': { schema: recordResponseSchema } } },
|
|
517
|
+
404: { description: 'Not found', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
518
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
tablesRoute.openapi(dbUpdateRecord, async (c) => {
|
|
523
|
+
const namespace = c.req.param('namespace')!;
|
|
524
|
+
const instanceId = c.req.param('instanceId')!;
|
|
525
|
+
const table = c.req.param('table')!;
|
|
526
|
+
const id = c.req.param('id')!;
|
|
527
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}/${id}`);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// ─── DELETE /{namespace}/{instanceId}/tables/{table}/{id} ─────────────
|
|
531
|
+
|
|
532
|
+
const dbDeleteRecord = createRoute({
|
|
533
|
+
operationId: 'dbDeleteRecord',
|
|
534
|
+
method: 'delete',
|
|
535
|
+
path: '/{namespace}/{instanceId}/tables/{table}/{id}',
|
|
536
|
+
tags: ['client'],
|
|
537
|
+
summary: 'Delete record from dynamic table',
|
|
538
|
+
request: {
|
|
539
|
+
params: dynamicTableIdParams,
|
|
540
|
+
},
|
|
541
|
+
responses: {
|
|
542
|
+
200: { description: 'Deleted', content: { 'application/json': { schema: jsonResponseSchema } } },
|
|
543
|
+
404: { description: 'Not found', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
544
|
+
403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
tablesRoute.openapi(dbDeleteRecord, async (c) => {
|
|
549
|
+
const namespace = c.req.param('namespace')!;
|
|
550
|
+
const instanceId = c.req.param('instanceId')!;
|
|
551
|
+
const table = c.req.param('table')!;
|
|
552
|
+
const id = c.req.param('id')!;
|
|
553
|
+
return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}/${id}`);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// ======================================================================
|
|
557
|
+
// Shared DO proxy logic
|
|
558
|
+
// ======================================================================
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Route a request to the correct backend based on provider config.
|
|
562
|
+
* - provider='do' (default): forwards to DatabaseDO instance
|
|
563
|
+
* - provider='neon'|'postgres': handles in Worker via postgres-handler
|
|
564
|
+
*
|
|
565
|
+
* Handles §36 canCreate 2-RTT flow for dynamic DOs.
|
|
566
|
+
*/
|
|
567
|
+
async function routeToDO(
|
|
568
|
+
c: Context<HonoEnv>,
|
|
569
|
+
namespace: string,
|
|
570
|
+
instanceId: string | undefined,
|
|
571
|
+
_tableName: string,
|
|
572
|
+
doPath: string,
|
|
573
|
+
): Promise<Response> {
|
|
574
|
+
const tableName = decodeURIComponent(_tableName);
|
|
575
|
+
// Check provider — route to D1 or PostgreSQL handler if not DO
|
|
576
|
+
const config = parseConfig(c.env);
|
|
577
|
+
const dbBlock = config.databases?.[namespace];
|
|
578
|
+
|
|
579
|
+
if (!dbBlock) {
|
|
580
|
+
return c.json({ code: 404, message: `Database '${namespace}' not found in config.` }, 404);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// D1 route: single-instance namespaces without dynamic instanceId
|
|
584
|
+
if (!instanceId && shouldRouteToD1(namespace, config)) {
|
|
585
|
+
return handleD1Request(c as unknown as Context<HonoEnv>, namespace, tableName, doPath);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// PostgreSQL route
|
|
589
|
+
const provider = dbBlock.provider;
|
|
590
|
+
if (provider === 'neon' || provider === 'postgres') {
|
|
591
|
+
return handlePgRequest(c as unknown as Context<HonoEnv>, namespace, tableName, doPath);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Build DO name: 'shared' | 'workspace:ws-456' (§2)
|
|
595
|
+
const doName = getDbDoName(namespace, instanceId);
|
|
596
|
+
|
|
597
|
+
const doId = c.env.DATABASE.idFromName(doName);
|
|
598
|
+
const stub = c.env.DATABASE.get(doId);
|
|
599
|
+
|
|
600
|
+
// Build forwarded request
|
|
601
|
+
const url = new URL(c.req.raw.url);
|
|
602
|
+
const doUrl = `http://do${doPath}${url.search}`;
|
|
603
|
+
|
|
604
|
+
const headers = new Headers(c.req.raw.headers);
|
|
605
|
+
headers.delete('X-EdgeBase-Internal');
|
|
606
|
+
headers.delete('X-Is-Service-Key');
|
|
607
|
+
headers.delete('X-DO-Create-Authorized');
|
|
608
|
+
headers.set('X-DO-Name', doName);
|
|
609
|
+
|
|
610
|
+
// Forward auth context to DO for hooks (#133 §6)
|
|
611
|
+
const auth = c.get('auth') as AuthContext | null | undefined;
|
|
612
|
+
if (auth !== undefined) {
|
|
613
|
+
headers.set('X-Auth-Context', JSON.stringify(auth));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// The trusted upstream rules middleware validates scoped/constrained Service Keys
|
|
617
|
+
// and pins the bypass decision on the request context. Downstream handlers and DOs
|
|
618
|
+
// must consume that decision instead of re-running a wildcard scope check.
|
|
619
|
+
const isServiceKey = c.get('isServiceKey' as never) === true;
|
|
620
|
+
if (isServiceKey) {
|
|
621
|
+
headers.set('X-Is-Service-Key', 'true');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const init: RequestInit = { method: c.req.raw.method, headers };
|
|
625
|
+
// Always pre-read body as text for non-GET/HEAD methods.
|
|
626
|
+
// ReadableStream is consumed by zod-validator middleware, so we
|
|
627
|
+
// reconstruct from Hono's cached parsed body (c.req.json()).
|
|
628
|
+
let bodyText: string | null = null;
|
|
629
|
+
if (c.req.raw.method !== 'GET' && c.req.raw.method !== 'HEAD') {
|
|
630
|
+
try {
|
|
631
|
+
const json = await c.req.json();
|
|
632
|
+
bodyText = JSON.stringify(json);
|
|
633
|
+
} catch {
|
|
634
|
+
// Body might be empty or non-JSON — default to empty object
|
|
635
|
+
// so DO doesn't crash with "Unexpected end of JSON input"
|
|
636
|
+
bodyText = '{}';
|
|
637
|
+
}
|
|
638
|
+
init.body = bodyText;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Retry on DO reset only for idempotent (read) methods.
|
|
642
|
+
// Writes are not retried to avoid duplicate side-effects (hooks, triggers,
|
|
643
|
+
// database-live events) and non-idempotent ops ($op: increment).
|
|
644
|
+
const safeToRetry = c.req.raw.method === 'GET' || c.req.raw.method === 'HEAD';
|
|
645
|
+
const res = await fetchDOWithRetry(stub, doUrl, {
|
|
646
|
+
method: c.req.raw.method,
|
|
647
|
+
headers,
|
|
648
|
+
body: bodyText,
|
|
649
|
+
}, { safeToRetry });
|
|
650
|
+
|
|
651
|
+
// §36: Handle needsCreate 2-RTT flow for dynamic DOs (not 'shared' or static)
|
|
652
|
+
if (res.status === 201 && instanceId) {
|
|
653
|
+
const body = await res.clone().json().catch(() => null) as
|
|
654
|
+
| { needsCreate?: boolean; namespace?: string; id?: string }
|
|
655
|
+
| null;
|
|
656
|
+
if (body?.needsCreate) {
|
|
657
|
+
// Evaluate DbLevelRules.canCreate(auth, id) in Worker (#133 §36)
|
|
658
|
+
const config = parseConfig(c.env);
|
|
659
|
+
const dbBlock = config.databases?.[namespace];
|
|
660
|
+
const canCreateFn = dbBlock?.access?.canCreate;
|
|
661
|
+
|
|
662
|
+
// Internal/admin DB proxy calls already bypass row-level rules.
|
|
663
|
+
// Dynamic DB bootstrap must honor that bypass too, otherwise
|
|
664
|
+
// context.admin.db(namespace, id).table(...).insert() fails on first write.
|
|
665
|
+
let allowed = isServiceKey;
|
|
666
|
+
if (!allowed && canCreateFn) {
|
|
667
|
+
try {
|
|
668
|
+
allowed = await Promise.resolve(canCreateFn(auth ?? null, body.id ?? instanceId));
|
|
669
|
+
} catch {
|
|
670
|
+
allowed = false; // fail-closed
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (!allowed) {
|
|
675
|
+
return c.json(
|
|
676
|
+
{ code: 403, message: 'DB creation not allowed.', error: 'CANNOT_CREATE_DB' },
|
|
677
|
+
403,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Authorized — retry with X-DO-Create-Authorized header
|
|
682
|
+
const retryHeaders = new Headers(headers);
|
|
683
|
+
retryHeaders.set('X-DO-Create-Authorized', '1');
|
|
684
|
+
const retryInit: RequestInit = { method: c.req.raw.method, headers: retryHeaders };
|
|
685
|
+
// BUG-008 fix: use pre-read body text (stream already consumed above)
|
|
686
|
+
if (c.req.raw.method !== 'GET' && c.req.raw.method !== 'HEAD') {
|
|
687
|
+
retryInit.body = bodyText ?? null;
|
|
688
|
+
}
|
|
689
|
+
// needsCreate 2-RTT: DO is empty at this point, safe to retry
|
|
690
|
+
return fetchDOWithRetry(stub, doUrl, {
|
|
691
|
+
method: c.req.raw.method,
|
|
692
|
+
headers: retryHeaders,
|
|
693
|
+
body: (c.req.raw.method !== 'GET' && c.req.raw.method !== 'HEAD') ? (bodyText ?? null) : null,
|
|
694
|
+
}, { safeToRetry: true });
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return res;
|
|
699
|
+
}
|