@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,538 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema → DDL conversion engine.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* 1. Convert table schema to CREATE TABLE DDL
|
|
6
|
+
* 2. Generate index DDL (single/composite, unique)
|
|
7
|
+
* 3. Generate FTS5 virtual table DDL
|
|
8
|
+
* 4. Inject auto fields (id, createdAt, updatedAt)
|
|
9
|
+
* 6. Hash schemas for Lazy Schema Init change detection
|
|
10
|
+
*/
|
|
11
|
+
import type {
|
|
12
|
+
TableConfig,
|
|
13
|
+
SchemaField,
|
|
14
|
+
IndexConfig,
|
|
15
|
+
FkReference,
|
|
16
|
+
} from '@edge-base/shared';
|
|
17
|
+
|
|
18
|
+
// ─── Type Mapping ───
|
|
19
|
+
|
|
20
|
+
const TYPE_MAP: Record<string, string> = {
|
|
21
|
+
string: 'TEXT',
|
|
22
|
+
text: 'TEXT',
|
|
23
|
+
number: 'REAL',
|
|
24
|
+
boolean: 'INTEGER',
|
|
25
|
+
datetime: 'TEXT',
|
|
26
|
+
json: 'TEXT',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Auth users are stored in AUTH_DB, so app tables can only keep logical references.
|
|
30
|
+
const AUTH_LOGICAL_REFERENCE_TABLES = new Set(['users', '_users', '_users_public']);
|
|
31
|
+
|
|
32
|
+
// ─── Auto Fields ───
|
|
33
|
+
|
|
34
|
+
const AUTO_FIELDS: Record<string, SchemaField> = {
|
|
35
|
+
id: { type: 'string', primaryKey: true },
|
|
36
|
+
createdAt: { type: 'datetime' },
|
|
37
|
+
updatedAt: { type: 'datetime', onUpdate: 'now' },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ─── System Table DDL ───
|
|
41
|
+
|
|
42
|
+
/** DDL for _meta table — exists on ALL Database DO instances. */
|
|
43
|
+
export const META_TABLE_DDL = `CREATE TABLE IF NOT EXISTS _meta (
|
|
44
|
+
key TEXT PRIMARY KEY,
|
|
45
|
+
value TEXT NOT NULL
|
|
46
|
+
);`;
|
|
47
|
+
|
|
48
|
+
// ─── Schema → DDL Conversion ───
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build the effective schema with auto fields injected.
|
|
52
|
+
* AutoFields can be disabled by setting them to `false` in schema.
|
|
53
|
+
* Type override of auto-fields is not supported — only `false` is allowed.
|
|
54
|
+
* When schema is undefined (schemaless CRUD,), returns auto-fields only.
|
|
55
|
+
*/
|
|
56
|
+
export function buildEffectiveSchema(
|
|
57
|
+
userSchema?: Record<string, SchemaField | false>,
|
|
58
|
+
): Record<string, SchemaField> {
|
|
59
|
+
const effective: Record<string, SchemaField> = {};
|
|
60
|
+
|
|
61
|
+
// Schemaless: return auto-fields only
|
|
62
|
+
if (!userSchema) {
|
|
63
|
+
for (const [name, field] of Object.entries(AUTO_FIELDS)) {
|
|
64
|
+
effective[name] = { ...field };
|
|
65
|
+
}
|
|
66
|
+
return effective;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Inject auto fields — only `false` disables them, type override is blocked
|
|
70
|
+
for (const [name, field] of Object.entries(AUTO_FIELDS)) {
|
|
71
|
+
if (userSchema[name] === false) {
|
|
72
|
+
continue; // Disabled
|
|
73
|
+
}
|
|
74
|
+
effective[name] = { ...field };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Add user fields (excluding auto field names already handled)
|
|
78
|
+
for (const [name, field] of Object.entries(userSchema)) {
|
|
79
|
+
if (name in AUTO_FIELDS) continue; // Already handled above
|
|
80
|
+
if (field === false) continue;
|
|
81
|
+
effective[name] = field;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return effective;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Generate CREATE TABLE DDL for a table.
|
|
89
|
+
*/
|
|
90
|
+
export function generateCreateTableDDL(
|
|
91
|
+
tableName: string,
|
|
92
|
+
config: TableConfig,
|
|
93
|
+
): string {
|
|
94
|
+
const schema = buildEffectiveSchema(config.schema);
|
|
95
|
+
const columns: string[] = [];
|
|
96
|
+
|
|
97
|
+
for (const [name, field] of Object.entries(schema)) {
|
|
98
|
+
columns.push(buildColumnDef(name, field));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return `CREATE TABLE IF NOT EXISTS ${esc(tableName)} (\n ${columns.join(',\n ')}\n);`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build a single column definition.
|
|
106
|
+
*/
|
|
107
|
+
function buildColumnDef(name: string, field: SchemaField): string {
|
|
108
|
+
const parts: string[] = [esc(name), TYPE_MAP[field.type] || 'TEXT'];
|
|
109
|
+
|
|
110
|
+
if (field.primaryKey) {
|
|
111
|
+
parts.push('PRIMARY KEY');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (field.required && !field.primaryKey) {
|
|
115
|
+
parts.push('NOT NULL');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (field.unique) {
|
|
119
|
+
parts.push('UNIQUE');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (field.default !== undefined) {
|
|
123
|
+
parts.push(`DEFAULT ${formatDefault(field.default)}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const referenceClause = buildReferenceClause(field.references);
|
|
127
|
+
if (referenceClause) {
|
|
128
|
+
parts.push(referenceClause);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// SQLite inline CHECK constraint (#133 §35)
|
|
132
|
+
if (field.check) {
|
|
133
|
+
parts.push(`CHECK (${field.check})`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return parts.join(' ');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generate ALTER TABLE ADD COLUMN for new fields.
|
|
141
|
+
*/
|
|
142
|
+
export function generateAddColumnDDL(
|
|
143
|
+
tableName: string,
|
|
144
|
+
name: string,
|
|
145
|
+
field: SchemaField,
|
|
146
|
+
): string {
|
|
147
|
+
return `ALTER TABLE ${esc(tableName)} ADD COLUMN ${buildColumnDef(name, field)};`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── Index DDL ───
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Generate CREATE INDEX DDL for indexes.
|
|
154
|
+
*/
|
|
155
|
+
export function generateIndexDDL(
|
|
156
|
+
tableName: string,
|
|
157
|
+
indexes: IndexConfig[],
|
|
158
|
+
): string[] {
|
|
159
|
+
return indexes.map((idx, _i) => {
|
|
160
|
+
const indexName = `idx_${tableName}_${idx.fields.join('_')}`;
|
|
161
|
+
const unique = idx.unique ? 'UNIQUE ' : '';
|
|
162
|
+
const fields = idx.fields.map(esc).join(', ');
|
|
163
|
+
return `CREATE ${unique}INDEX IF NOT EXISTS ${esc(indexName)} ON ${esc(tableName)}(${fields});`;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── FTS5 DDL ───
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Generate FTS5 virtual table DDL with trigram tokenizer.
|
|
171
|
+
* @param tableName Base table name
|
|
172
|
+
* @param ftsFields Fields to index for full-text search
|
|
173
|
+
*/
|
|
174
|
+
export function generateFTS5DDL(
|
|
175
|
+
tableName: string,
|
|
176
|
+
ftsFields: string[],
|
|
177
|
+
): string {
|
|
178
|
+
const ftsTableName = `${tableName}_fts`;
|
|
179
|
+
const fields = ftsFields.join(', ');
|
|
180
|
+
// content-sync with base table
|
|
181
|
+
return `CREATE VIRTUAL TABLE IF NOT EXISTS ${esc(ftsTableName)} USING fts5(${fields}, content='${tableName}', content_rowid='rowid', tokenize='trigram');`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Generate FTS5 triggers for auto-sync with base table.
|
|
186
|
+
*/
|
|
187
|
+
export function generateFTS5Triggers(
|
|
188
|
+
tableName: string,
|
|
189
|
+
ftsFields: string[],
|
|
190
|
+
): string[] {
|
|
191
|
+
const ftsTableName = `${tableName}_fts`;
|
|
192
|
+
const newFields = ftsFields.map(f => `new.${esc(f)}`).join(', ');
|
|
193
|
+
const oldFields = ftsFields.map(f => `old.${esc(f)}`).join(', ');
|
|
194
|
+
|
|
195
|
+
return [
|
|
196
|
+
// INSERT trigger
|
|
197
|
+
`CREATE TRIGGER IF NOT EXISTS ${esc(`${tableName}_ai`)} AFTER INSERT ON ${esc(tableName)} BEGIN
|
|
198
|
+
INSERT INTO ${esc(ftsTableName)}(rowid, ${ftsFields.map(esc).join(', ')}) VALUES (new.rowid, ${newFields});
|
|
199
|
+
END;`,
|
|
200
|
+
// DELETE trigger
|
|
201
|
+
`CREATE TRIGGER IF NOT EXISTS ${esc(`${tableName}_ad`)} AFTER DELETE ON ${esc(tableName)} BEGIN
|
|
202
|
+
INSERT INTO ${esc(ftsTableName)}(${esc(ftsTableName)}, rowid, ${ftsFields.map(esc).join(', ')}) VALUES ('delete', old.rowid, ${oldFields});
|
|
203
|
+
END;`,
|
|
204
|
+
// UPDATE trigger
|
|
205
|
+
`CREATE TRIGGER IF NOT EXISTS ${esc(`${tableName}_au`)} AFTER UPDATE ON ${esc(tableName)} BEGIN
|
|
206
|
+
INSERT INTO ${esc(ftsTableName)}(${esc(ftsTableName)}, rowid, ${ftsFields.map(esc).join(', ')}) VALUES ('delete', old.rowid, ${oldFields});
|
|
207
|
+
INSERT INTO ${esc(ftsTableName)}(rowid, ${ftsFields.map(esc).join(', ')}) VALUES (new.rowid, ${newFields});
|
|
208
|
+
END;`,
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Schema Hashing ───
|
|
213
|
+
|
|
214
|
+
function deepSort(obj: unknown): unknown {
|
|
215
|
+
if (obj === null || typeof obj !== 'object') return obj;
|
|
216
|
+
if (Array.isArray(obj)) return obj.map(deepSort);
|
|
217
|
+
const sorted: Record<string, unknown> = {};
|
|
218
|
+
for (const key of Object.keys(obj as Record<string, unknown>).sort()) {
|
|
219
|
+
sorted[key] = deepSort((obj as Record<string, unknown>)[key]);
|
|
220
|
+
}
|
|
221
|
+
return sorted;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Compute a deterministic hash of a table schema for change detection.
|
|
226
|
+
* Uses JSON serialization with deep sorted keys + SHA-256.
|
|
227
|
+
*/
|
|
228
|
+
export async function computeSchemaHash(
|
|
229
|
+
config: TableConfig,
|
|
230
|
+
): Promise<string> {
|
|
231
|
+
const schemaOnly = { schema: config.schema ?? {} };
|
|
232
|
+
const str = JSON.stringify(deepSort(schemaOnly));
|
|
233
|
+
const data = new TextEncoder().encode(str);
|
|
234
|
+
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
235
|
+
const bytes = new Uint8Array(hash);
|
|
236
|
+
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Synchronous schema hash using simple djb2 — for use in DO constructor.
|
|
241
|
+
* Only hashes the `schema` field (table column definitions), NOT rules/hooks
|
|
242
|
+
* (functions serialize to undefined in JSON.stringify).
|
|
243
|
+
* Schema changes trigger DDL migration; rules/hooks changes are picked up
|
|
244
|
+
* by Worker redeployment without DDL changes. (#133 §27)
|
|
245
|
+
*/
|
|
246
|
+
export function computeSchemaHashSync(
|
|
247
|
+
config: TableConfig,
|
|
248
|
+
): string {
|
|
249
|
+
// Only schema field — rules/hooks are functions and would serialize to undefined
|
|
250
|
+
const schemaOnly = { schema: config.schema ?? {} };
|
|
251
|
+
const str = JSON.stringify(deepSort(schemaOnly));
|
|
252
|
+
let hash = 5381;
|
|
253
|
+
for (let i = 0; i < str.length; i++) {
|
|
254
|
+
hash = ((hash << 5) + hash + str.charCodeAt(i)) & 0xffffffff;
|
|
255
|
+
}
|
|
256
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ─── Utility ───
|
|
260
|
+
|
|
261
|
+
/** Escape identifiers (table/column names). */
|
|
262
|
+
function esc(name: string): string {
|
|
263
|
+
// Double-quote escaping for SQLite identifiers
|
|
264
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function extractReferenceTable(reference: string | FkReference | undefined): string | null {
|
|
268
|
+
if (!reference) return null;
|
|
269
|
+
if (typeof reference === 'string') {
|
|
270
|
+
const match = reference.trim().match(/^(\w+)(?:\((\w+)\))?$/);
|
|
271
|
+
return match?.[1] ?? null;
|
|
272
|
+
}
|
|
273
|
+
return reference.table;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function isLogicalOnlyReference(reference: string | FkReference | undefined): boolean {
|
|
277
|
+
const table = extractReferenceTable(reference);
|
|
278
|
+
return table !== null && AUTH_LOGICAL_REFERENCE_TABLES.has(table);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function buildReferenceClause(reference: string | FkReference | undefined): string | null {
|
|
282
|
+
if (!reference || isLogicalOnlyReference(reference)) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (typeof reference === 'string') {
|
|
287
|
+
const ref = reference.trim();
|
|
288
|
+
if (ref.includes('(')) {
|
|
289
|
+
const match = ref.match(/^(\w+)\((\w+)\)$/);
|
|
290
|
+
if (match) {
|
|
291
|
+
return `REFERENCES ${esc(match[1])}(${esc(match[2])}) ON DELETE CASCADE`;
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
return `REFERENCES ${esc(ref)}("id") ON DELETE SET NULL`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const col = reference.column ?? 'id';
|
|
299
|
+
const delAction = reference.onDelete ? ` ON DELETE ${reference.onDelete}` : '';
|
|
300
|
+
const updAction = reference.onUpdate ? ` ON UPDATE ${reference.onUpdate}` : '';
|
|
301
|
+
return `REFERENCES ${esc(reference.table)}(${esc(col)})${delAction}${updAction}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function formatDefault(val: unknown): string {
|
|
305
|
+
if (typeof val === 'string') return `'${val.replace(/'/g, "''")}'`;
|
|
306
|
+
if (typeof val === 'boolean') return val ? '1' : '0';
|
|
307
|
+
if (val === null) return 'NULL';
|
|
308
|
+
return String(val);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get all DDL statements needed for a single table (#133 §26).
|
|
313
|
+
* Returns array of DDL strings to execute in order.
|
|
314
|
+
*/
|
|
315
|
+
export function generateTableDDL(
|
|
316
|
+
tableName: string,
|
|
317
|
+
config: TableConfig,
|
|
318
|
+
): string[] {
|
|
319
|
+
const ddl: string[] = [];
|
|
320
|
+
|
|
321
|
+
// 1. CREATE TABLE
|
|
322
|
+
ddl.push(generateCreateTableDDL(tableName, config));
|
|
323
|
+
|
|
324
|
+
// 2. Indexes
|
|
325
|
+
if (config.indexes?.length) {
|
|
326
|
+
ddl.push(...generateIndexDDL(tableName, config.indexes));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 3. FTS5
|
|
330
|
+
if (config.fts?.length) {
|
|
331
|
+
ddl.push(generateFTS5DDL(tableName, config.fts));
|
|
332
|
+
ddl.push(...generateFTS5Triggers(tableName, config.fts));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return ddl;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
339
|
+
// PostgreSQL DDL Generation
|
|
340
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
341
|
+
|
|
342
|
+
// ─── PostgreSQL Type Mapping ───
|
|
343
|
+
|
|
344
|
+
const PG_TYPE_MAP: Record<string, string> = {
|
|
345
|
+
string: 'TEXT',
|
|
346
|
+
text: 'TEXT',
|
|
347
|
+
number: 'DOUBLE PRECISION',
|
|
348
|
+
boolean: 'BOOLEAN',
|
|
349
|
+
datetime: 'TIMESTAMPTZ',
|
|
350
|
+
json: 'JSONB',
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// ─── PostgreSQL System Table DDL ───
|
|
354
|
+
|
|
355
|
+
/** DDL for _meta table on PostgreSQL databases. */
|
|
356
|
+
export const PG_META_TABLE_DDL = `CREATE TABLE IF NOT EXISTS _meta (
|
|
357
|
+
key TEXT PRIMARY KEY,
|
|
358
|
+
value TEXT NOT NULL
|
|
359
|
+
);`;
|
|
360
|
+
|
|
361
|
+
// ─── PostgreSQL Schema → DDL Conversion ───
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Generate PostgreSQL CREATE TABLE DDL for a table.
|
|
365
|
+
*/
|
|
366
|
+
export function generatePgCreateTableDDL(
|
|
367
|
+
tableName: string,
|
|
368
|
+
config: TableConfig,
|
|
369
|
+
): string {
|
|
370
|
+
const schema = buildEffectiveSchema(config.schema);
|
|
371
|
+
const columns: string[] = [];
|
|
372
|
+
|
|
373
|
+
for (const [name, field] of Object.entries(schema)) {
|
|
374
|
+
columns.push(buildPgColumnDef(name, field));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return `CREATE TABLE IF NOT EXISTS ${esc(tableName)} (\n ${columns.join(',\n ')}\n);`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Build a single PostgreSQL column definition.
|
|
382
|
+
*/
|
|
383
|
+
function buildPgColumnDef(name: string, field: SchemaField): string {
|
|
384
|
+
const parts: string[] = [esc(name), PG_TYPE_MAP[field.type] || 'TEXT'];
|
|
385
|
+
|
|
386
|
+
if (field.primaryKey) {
|
|
387
|
+
parts.push('PRIMARY KEY');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (field.required && !field.primaryKey) {
|
|
391
|
+
parts.push('NOT NULL');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (field.unique) {
|
|
395
|
+
parts.push('UNIQUE');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (field.default !== undefined) {
|
|
399
|
+
parts.push(`DEFAULT ${formatPgDefault(field.default)}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const referenceClause = buildReferenceClause(field.references);
|
|
403
|
+
if (referenceClause) {
|
|
404
|
+
parts.push(referenceClause);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// CHECK constraint (same syntax as SQLite)
|
|
408
|
+
if (field.check) {
|
|
409
|
+
parts.push(`CHECK (${field.check})`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return parts.join(' ');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Generate PostgreSQL ALTER TABLE ADD COLUMN for new fields.
|
|
417
|
+
*/
|
|
418
|
+
export function generatePgAddColumnDDL(
|
|
419
|
+
tableName: string,
|
|
420
|
+
name: string,
|
|
421
|
+
field: SchemaField,
|
|
422
|
+
): string {
|
|
423
|
+
return `ALTER TABLE ${esc(tableName)} ADD COLUMN ${buildPgColumnDef(name, field)};`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ─── PostgreSQL Index DDL ───
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Generate PostgreSQL CREATE INDEX DDL.
|
|
430
|
+
* Syntax is identical to SQLite — kept separate for future B-tree hints.
|
|
431
|
+
*/
|
|
432
|
+
export function generatePgIndexDDL(
|
|
433
|
+
tableName: string,
|
|
434
|
+
indexes: IndexConfig[],
|
|
435
|
+
): string[] {
|
|
436
|
+
return indexes.map((idx) => {
|
|
437
|
+
const indexName = `idx_${tableName}_${idx.fields.join('_')}`;
|
|
438
|
+
const unique = idx.unique ? 'UNIQUE ' : '';
|
|
439
|
+
const fields = idx.fields.map(esc).join(', ');
|
|
440
|
+
return `CREATE ${unique}INDEX IF NOT EXISTS ${esc(indexName)} ON ${esc(tableName)}(${fields});`;
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ─── PostgreSQL FTS (tsvector + GIN) ───
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Generate PostgreSQL full-text search DDL:
|
|
448
|
+
* 1. Add `_fts` tsvector column to the table
|
|
449
|
+
* 2. Create GIN index on the tsvector column
|
|
450
|
+
* 3. Create trigger function to auto-update tsvector on write
|
|
451
|
+
* 4. Create BEFORE INSERT OR UPDATE trigger
|
|
452
|
+
* 5. Backfill existing rows (harmless on empty tables)
|
|
453
|
+
*
|
|
454
|
+
* Uses 'simple' text search config for language-agnostic matching
|
|
455
|
+
* (closest equivalent to SQLite FTS5 trigram tokenizer).
|
|
456
|
+
*/
|
|
457
|
+
export function generatePgFTSDDL(
|
|
458
|
+
tableName: string,
|
|
459
|
+
ftsFields: string[],
|
|
460
|
+
): string[] {
|
|
461
|
+
const ddl: string[] = [];
|
|
462
|
+
const ftsCol = '_fts';
|
|
463
|
+
const triggerName = `${tableName}_fts_update`;
|
|
464
|
+
const funcName = `${tableName}_fts_trigger`;
|
|
465
|
+
const indexName = `idx_${tableName}_fts`;
|
|
466
|
+
|
|
467
|
+
// Coalesce expression: coalesce(NEW."field", '') || ' ' || ...
|
|
468
|
+
const newCoalesce = ftsFields
|
|
469
|
+
.map(f => `coalesce(NEW.${esc(f)}, '')`)
|
|
470
|
+
.join(` || ' ' || `);
|
|
471
|
+
const bareCoalesce = ftsFields
|
|
472
|
+
.map(f => `coalesce(${esc(f)}, '')`)
|
|
473
|
+
.join(` || ' ' || `);
|
|
474
|
+
|
|
475
|
+
// 1. Add tsvector column (IF NOT EXISTS — PG 9.6+)
|
|
476
|
+
ddl.push(
|
|
477
|
+
`ALTER TABLE ${esc(tableName)} ADD COLUMN IF NOT EXISTS ${esc(ftsCol)} tsvector;`,
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
// 2. GIN index for fast @@ queries
|
|
481
|
+
ddl.push(
|
|
482
|
+
`CREATE INDEX IF NOT EXISTS ${esc(indexName)} ON ${esc(tableName)} USING gin(${esc(ftsCol)});`,
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
// 3. Trigger function — builds tsvector from indexed fields
|
|
486
|
+
ddl.push(
|
|
487
|
+
`CREATE OR REPLACE FUNCTION ${esc(funcName)}() RETURNS trigger AS $$\nBEGIN\n NEW.${esc(ftsCol)} := to_tsvector('simple', ${newCoalesce});\n RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;`,
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
// 4. Trigger (drop + create to handle field list changes)
|
|
491
|
+
ddl.push(
|
|
492
|
+
`DROP TRIGGER IF EXISTS ${esc(triggerName)} ON ${esc(tableName)};\nCREATE TRIGGER ${esc(triggerName)} BEFORE INSERT OR UPDATE ON ${esc(tableName)}\n FOR EACH ROW EXECUTE FUNCTION ${esc(funcName)}();`,
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
// 5. Backfill existing rows
|
|
496
|
+
ddl.push(
|
|
497
|
+
`UPDATE ${esc(tableName)} SET ${esc(ftsCol)} = to_tsvector('simple', ${bareCoalesce});`,
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
return ddl;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ─── PostgreSQL Full Table DDL ───
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Get all DDL statements needed for a single PostgreSQL table.
|
|
507
|
+
* Returns array of DDL strings to execute in order.
|
|
508
|
+
*/
|
|
509
|
+
export function generatePgTableDDL(
|
|
510
|
+
tableName: string,
|
|
511
|
+
config: TableConfig,
|
|
512
|
+
): string[] {
|
|
513
|
+
const ddl: string[] = [];
|
|
514
|
+
|
|
515
|
+
// 1. CREATE TABLE
|
|
516
|
+
ddl.push(generatePgCreateTableDDL(tableName, config));
|
|
517
|
+
|
|
518
|
+
// 2. Indexes
|
|
519
|
+
if (config.indexes?.length) {
|
|
520
|
+
ddl.push(...generatePgIndexDDL(tableName, config.indexes));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// 3. FTS (tsvector + GIN + trigger)
|
|
524
|
+
if (config.fts?.length) {
|
|
525
|
+
ddl.push(...generatePgFTSDDL(tableName, config.fts));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return ddl;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ─── PostgreSQL Default Value Formatting ───
|
|
532
|
+
|
|
533
|
+
function formatPgDefault(val: unknown): string {
|
|
534
|
+
if (typeof val === 'string') return `'${val.replace(/'/g, "''")}'`;
|
|
535
|
+
if (typeof val === 'boolean') return val ? 'TRUE' : 'FALSE';
|
|
536
|
+
if (val === null) return 'NULL';
|
|
537
|
+
return String(val);
|
|
538
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Zod schemas — single source of truth for request/response shapes.
|
|
3
|
+
*
|
|
4
|
+
* These schemas serve three purposes:
|
|
5
|
+
* 1. Runtime validation (Zod)
|
|
6
|
+
* 2. OpenAPI spec generation (@hono/zod-openapi)
|
|
7
|
+
* 3. SDK type generation (via OpenAPI)
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANT: queryParamsSchema keys MUST stay in sync with QUERY_PARAM_KEYS
|
|
10
|
+
* in query-engine.ts. The 3-way sync test in query.test.ts enforces this.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from '@hono/zod-openapi';
|
|
13
|
+
|
|
14
|
+
// ─── Query Parameters ───────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/** Zod schema for REST query parameters. Keys mirror QUERY_PARAM_KEYS. */
|
|
17
|
+
export const queryParamsSchema = z.object({
|
|
18
|
+
limit: z.string().optional().openapi({ description: 'Max items to return', example: '20' }),
|
|
19
|
+
offset: z.string().optional().openapi({ description: 'Offset for pagination', example: '0' }),
|
|
20
|
+
page: z.string().optional().openapi({ description: 'Page number (1-based)', example: '1' }),
|
|
21
|
+
perPage: z.string().optional().openapi({ description: 'Items per page', example: '20' }),
|
|
22
|
+
after: z.string().optional().openapi({ description: 'Cursor for next page' }),
|
|
23
|
+
before: z.string().optional().openapi({ description: 'Cursor for previous page' }),
|
|
24
|
+
sort: z.string().optional().openapi({ description: 'Sort: field:asc,field2:desc', example: 'createdAt:desc' }),
|
|
25
|
+
filter: z.string().optional().openapi({ description: 'JSON-encoded filter tuples', example: '[["status","==","active"]]' }),
|
|
26
|
+
orFilter: z.string().optional().openapi({ description: 'JSON-encoded OR filter tuples' }),
|
|
27
|
+
fields: z.string().optional().openapi({ description: 'Comma-separated field names to return' }),
|
|
28
|
+
search: z.string().optional().openapi({ description: 'Full-text search query' }),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ─── Response Schemas ───────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** List response — shared by all paginated endpoints. */
|
|
34
|
+
export const listResponseSchema = z.object({
|
|
35
|
+
items: z.array(z.record(z.string(), z.unknown())).openapi({ description: 'Result items' }),
|
|
36
|
+
total: z.number().nullable().openapi({ description: 'Total count (offset mode)' }),
|
|
37
|
+
hasMore: z.boolean().nullable().openapi({ description: 'More pages available (cursor mode)' }),
|
|
38
|
+
cursor: z.string().nullable().openapi({ description: 'Cursor for next page' }),
|
|
39
|
+
page: z.number().nullable().openapi({ description: 'Current page (offset mode)' }),
|
|
40
|
+
perPage: z.number().nullable().openapi({ description: 'Items per page (offset mode)' }),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** Standard error response. */
|
|
44
|
+
export const errorResponseSchema = z.object({
|
|
45
|
+
code: z.number().openapi({ description: 'HTTP status code', example: 400 }),
|
|
46
|
+
message: z.string().openapi({ description: 'Human-readable message', example: 'Invalid request body' }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** Single record response (generic). */
|
|
50
|
+
export const recordResponseSchema = z.record(z.string(), z.unknown()).openapi({
|
|
51
|
+
description: 'A single database record',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** Success response for mutations. */
|
|
55
|
+
export const successResponseSchema = z.object({
|
|
56
|
+
success: z.literal(true),
|
|
57
|
+
}).openapi({ description: 'Operation completed successfully' });
|
|
58
|
+
|
|
59
|
+
/** Health check response. */
|
|
60
|
+
export const healthResponseSchema = z.object({
|
|
61
|
+
status: z.string().openapi({ example: 'ok' }),
|
|
62
|
+
timestamp: z.string().openapi({ example: '2026-01-01T00:00:00.000Z' }),
|
|
63
|
+
version: z.string().optional().openapi({ example: '0.1.0' }),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ─── Shared Parameter Schemas ─────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export const idParamSchema = z.object({ id: z.string().openapi({ description: 'Record ID' }) });
|
|
69
|
+
export const nameParamSchema = z.object({ name: z.string().openapi({ description: 'Resource name' }) });
|
|
70
|
+
export const bucketParamSchema = z.object({ bucket: z.string().openapi({ description: 'Storage bucket name' }) });
|
|
71
|
+
export const providerParamSchema = z.object({ provider: z.string().openapi({ description: 'OAuth provider name' }) });
|
|
72
|
+
|
|
73
|
+
// ─── Generic Response Schemas ─────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/** Generic JSON response (for endpoints with dynamic shapes). */
|
|
76
|
+
export const jsonResponseSchema = z.record(z.string(), z.unknown()).openapi({ description: 'JSON response' });
|
|
77
|
+
|
|
78
|
+
/** Simple ok response. */
|
|
79
|
+
export const okResponseSchema = z.object({
|
|
80
|
+
ok: z.literal(true),
|
|
81
|
+
}).openapi({ description: 'Operation succeeded' });
|
|
82
|
+
|
|
83
|
+
// ─── Shared Body Schemas ──────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
export const d1BodySchema = z.object({
|
|
86
|
+
query: z.string().openapi({ description: 'SQL query string' }),
|
|
87
|
+
params: z.array(z.unknown()).optional().openapi({ description: 'Bind parameters' }),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export const sqlBodySchema = z.object({
|
|
91
|
+
namespace: z.string().openapi({ description: 'Database namespace' }),
|
|
92
|
+
id: z.string().optional().openapi({ description: 'Instance ID (for dynamic DBs)' }),
|
|
93
|
+
sql: z.string().openapi({ description: 'SQL query' }),
|
|
94
|
+
params: z.array(z.unknown()).optional().openapi({ description: 'Bind parameters' }),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export const kvBodySchema = z.object({
|
|
98
|
+
action: z.enum(['get', 'set', 'delete', 'list']).openapi({ description: 'KV operation' }),
|
|
99
|
+
key: z.string().optional().openapi({ description: 'KV key' }),
|
|
100
|
+
value: z.string().optional().openapi({ description: 'Value to set' }),
|
|
101
|
+
ttl: z.number().optional().openapi({ description: 'TTL in seconds' }),
|
|
102
|
+
prefix: z.string().optional().openapi({ description: 'Key prefix for list' }),
|
|
103
|
+
limit: z.number().optional().openapi({ description: 'Max keys for list' }),
|
|
104
|
+
cursor: z.string().optional().openapi({ description: 'Pagination cursor for list' }),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
export const vectorizeBodySchema = z.object({
|
|
108
|
+
action: z.enum(['upsert', 'insert', 'search', 'queryById', 'getByIds', 'delete', 'describe']).openapi({ description: 'Vectorize operation' }),
|
|
109
|
+
vectors: z.array(z.object({
|
|
110
|
+
id: z.string(),
|
|
111
|
+
values: z.array(z.number()),
|
|
112
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
113
|
+
namespace: z.string().optional(),
|
|
114
|
+
})).optional(),
|
|
115
|
+
vector: z.array(z.number()).optional(),
|
|
116
|
+
vectorId: z.string().optional(),
|
|
117
|
+
topK: z.number().optional(),
|
|
118
|
+
filter: z.record(z.string(), z.unknown()).optional(),
|
|
119
|
+
ids: z.array(z.string()).optional(),
|
|
120
|
+
namespace: z.string().optional(),
|
|
121
|
+
returnValues: z.boolean().optional(),
|
|
122
|
+
returnMetadata: z.union([z.boolean(), z.enum(['all', 'indexed', 'none'])]).optional(),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export const broadcastBodySchema = z.object({
|
|
126
|
+
channel: z.string().openapi({ description: 'Database live channel name' }),
|
|
127
|
+
event: z.string().openapi({ description: 'Event name' }),
|
|
128
|
+
payload: z.record(z.string(), z.unknown()).optional().openapi({ description: 'Event payload' }),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export const trackEventsBodySchema = z.object({
|
|
132
|
+
events: z.array(z.record(z.string(), z.unknown())).openapi({ description: 'Events to track' }),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ─── OpenAPIHono defaultHook ──────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Shared defaultHook for all OpenAPIHono instances.
|
|
139
|
+
* Returns Zod validation errors in the standard { code, message } format.
|
|
140
|
+
*/
|
|
141
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
142
|
+
export function zodDefaultHook(result: { success: boolean; error?: { issues?: Array<{ message: string }>; errors?: Array<{ message: string }> } }, c: any) {
|
|
143
|
+
if (!result.success) {
|
|
144
|
+
// Zod v4 uses `issues`, Zod v3 uses `errors`
|
|
145
|
+
const items = (result.error as any)?.issues ?? (result.error as any)?.errors ?? [];
|
|
146
|
+
return c.json({
|
|
147
|
+
code: 400,
|
|
148
|
+
message: items.map((e: any) => e.message).join(', '),
|
|
149
|
+
}, 400);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|