@edge-base/server 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/admin-build/_app/immutable/chunks/{C85dMlzL.js → 5RQRbp5q.js} +1 -1
  2. package/admin-build/_app/immutable/chunks/{B8DT4fss.js → BME_U9TJ.js} +1 -1
  3. package/admin-build/_app/immutable/chunks/{BaCHY17I.js → BYI6CUvd.js} +1 -1
  4. package/admin-build/_app/immutable/chunks/{BWyDPAjM.js → BgDzp0i0.js} +1 -1
  5. package/admin-build/_app/immutable/chunks/{c5iKSdWY.js → BjWZuf8W.js} +1 -1
  6. package/admin-build/_app/immutable/chunks/{g3ZZdY-r.js → C6lpZLE2.js} +1 -1
  7. package/admin-build/_app/immutable/chunks/{C-DsDCNG.js → D5GswVnI.js} +3 -3
  8. package/admin-build/_app/immutable/chunks/DBsVqhuh.js +1 -0
  9. package/admin-build/_app/immutable/chunks/{BEYYl662.js → DYaCRWMA.js} +1 -1
  10. package/admin-build/_app/immutable/chunks/D__dwMuW.js +1 -0
  11. package/admin-build/_app/immutable/chunks/{4vlsb8ej.js → Dj-E9-FO.js} +1 -1
  12. package/admin-build/_app/immutable/chunks/{kiJ6KthZ.js → Dj0QUuOf.js} +1 -1
  13. package/admin-build/_app/immutable/chunks/{BKXmgPq4.js → XQM1k9PM.js} +1 -1
  14. package/admin-build/_app/immutable/chunks/{CTngeX8H.js → fYEKMQ-Z.js} +1 -1
  15. package/admin-build/_app/immutable/chunks/{CPdXvRUb.js → g_-Kpxu3.js} +1 -1
  16. package/admin-build/_app/immutable/chunks/{DzXaj-Ja.js → wCNueVYy.js} +1 -1
  17. package/admin-build/_app/immutable/entry/{app.BZxfavhF.js → app.C8ylfBe6.js} +2 -2
  18. package/admin-build/_app/immutable/entry/start.CtsqDyfj.js +1 -0
  19. package/admin-build/_app/immutable/nodes/{0.DlsaydXO.js → 0.CJJ6HZbp.js} +1 -1
  20. package/admin-build/_app/immutable/nodes/{1.D2NWN5eG.js → 1.B4sI5cB4.js} +1 -1
  21. package/admin-build/_app/immutable/nodes/{10.EMDaN3nw.js → 10.D6hvCer6.js} +1 -1
  22. package/admin-build/_app/immutable/nodes/{11.BasqQ_o9.js → 11.Dx7b8aQ5.js} +1 -1
  23. package/admin-build/_app/immutable/nodes/{12.DO31Ljs7.js → 12.Bqmy5KIF.js} +1 -1
  24. package/admin-build/_app/immutable/nodes/{13.DhyAy-GZ.js → 13.CC6KpXgS.js} +1 -1
  25. package/admin-build/_app/immutable/nodes/{14.CLecGWc4.js → 14.yCo1Ix8E.js} +1 -1
  26. package/admin-build/_app/immutable/nodes/{15.B9kp3W4e.js → 15.co0UfPlh.js} +1 -1
  27. package/admin-build/_app/immutable/nodes/{16.Pu_8T3RI.js → 16.D0xkPUBW.js} +1 -1
  28. package/admin-build/_app/immutable/nodes/{17.DX4z43t6.js → 17.CebNqPeh.js} +1 -1
  29. package/admin-build/_app/immutable/nodes/{18.BKsSaxrr.js → 18.JUoLOZxh.js} +1 -1
  30. package/admin-build/_app/immutable/nodes/{19.DXNF1htN.js → 19.ND8kmQJe.js} +1 -1
  31. package/admin-build/_app/immutable/nodes/{20.VRVb0wee.js → 20.DYb-q3W8.js} +1 -1
  32. package/admin-build/_app/immutable/nodes/21.cz3IN9Cc.js +1 -0
  33. package/admin-build/_app/immutable/nodes/{22.DqZf4CtH.js → 22.UOzm8WYV.js} +1 -1
  34. package/admin-build/_app/immutable/nodes/{23.DtyxMiQG.js → 23.BLgq21om.js} +1 -1
  35. package/admin-build/_app/immutable/nodes/{24.CloWNmTd.js → 24.DN9usmUs.js} +1 -1
  36. package/admin-build/_app/immutable/nodes/{25.CnZWMq7_.js → 25.BddRfAyE.js} +1 -1
  37. package/admin-build/_app/immutable/nodes/{26.DrV7XOmf.js → 26.Dl6XHIeT.js} +1 -1
  38. package/admin-build/_app/immutable/nodes/{27.DV8L32OF.js → 27.D0iNwALG.js} +1 -1
  39. package/admin-build/_app/immutable/nodes/{28.Stil2D4u.js → 28.9dKQmdGi.js} +1 -1
  40. package/admin-build/_app/immutable/nodes/{29.Zsm1e5Dc.js → 29.wXzfJUXp.js} +1 -1
  41. package/admin-build/_app/immutable/nodes/{3.CKoj2vNz.js → 3.z8ut3jS-.js} +1 -1
  42. package/admin-build/_app/immutable/nodes/{30.Ni0k5bER.js → 30.BtZETNsL.js} +1 -1
  43. package/admin-build/_app/immutable/nodes/{31.mnqj9EbV.js → 31.CYonj2Jh.js} +1 -1
  44. package/admin-build/_app/immutable/nodes/{4.B_-z9AzT.js → 4.COtDPQ9b.js} +1 -1
  45. package/admin-build/_app/immutable/nodes/{5.yiZ72j4k.js → 5.CTRCeIhp.js} +1 -1
  46. package/admin-build/_app/immutable/nodes/{6.BqykybBG.js → 6.ChHi3QkR.js} +1 -1
  47. package/admin-build/_app/immutable/nodes/{7.BDAHlhsF.js → 7.CCMtr6Ac.js} +1 -1
  48. package/admin-build/_app/immutable/nodes/{8.D8Xvy0lH.js → 8.DpWJ-X_-.js} +1 -1
  49. package/admin-build/_app/immutable/nodes/{9.Dddmd7_F.js → 9.DOkvfmir.js} +1 -1
  50. package/admin-build/_app/version.json +1 -1
  51. package/admin-build/index.html +7 -7
  52. package/package.json +3 -3
  53. package/src/__tests__/admin-data-routes.test.ts +29 -0
  54. package/src/__tests__/d1-live-broadcast-verification.test.ts +271 -0
  55. package/src/__tests__/database-do-route-validation.test.ts +105 -0
  56. package/src/__tests__/database-live-do.test.ts +50 -0
  57. package/src/__tests__/database-live-emitter.test.ts +116 -1
  58. package/src/__tests__/database-live-route.test.ts +82 -0
  59. package/src/__tests__/do-router.test.ts +116 -0
  60. package/src/__tests__/error-format.test.ts +63 -0
  61. package/src/__tests__/functions-context.test.ts +674 -33
  62. package/src/__tests__/functions-d1-proxy.test.ts +54 -0
  63. package/src/__tests__/plugin-migration-routing.test.ts +32 -0
  64. package/src/__tests__/postgres-field-ops-compat.test.ts +110 -0
  65. package/src/__tests__/provider-aware-sql.test.ts +163 -0
  66. package/src/__tests__/room-auth-state-loss.test.ts +124 -0
  67. package/src/__tests__/runtime-surface-accounting.test.ts +0 -4
  68. package/src/__tests__/scheduled.test.ts +55 -0
  69. package/src/__tests__/service-key-db-proxy.test.ts +122 -1
  70. package/src/__tests__/sql-route.test.ts +252 -75
  71. package/src/__tests__/table-hook-runtime.test.ts +137 -0
  72. package/src/durable-objects/database-do.ts +36 -45
  73. package/src/durable-objects/database-live-do.ts +46 -1
  74. package/src/durable-objects/room-runtime-base.ts +26 -2
  75. package/src/durable-objects/rooms-do.ts +1 -1
  76. package/src/index.ts +12 -6
  77. package/src/lib/admin-db-target.ts +30 -74
  78. package/src/lib/d1-handler.ts +55 -35
  79. package/src/lib/database-live-emitter.ts +57 -16
  80. package/src/lib/do-router.ts +135 -3
  81. package/src/lib/functions.ts +215 -143
  82. package/src/lib/internal-transport.ts +28 -12
  83. package/src/lib/plugin-migration-routing.ts +28 -0
  84. package/src/lib/plugin-migrations.ts +38 -38
  85. package/src/lib/postgres-handler.ts +51 -31
  86. package/src/lib/provider-aware-sql.ts +831 -0
  87. package/src/lib/table-hook-runtime.ts +62 -0
  88. package/src/routes/admin.ts +41 -41
  89. package/src/routes/auth.ts +7 -2
  90. package/src/routes/database-live.ts +110 -12
  91. package/src/routes/sql.ts +64 -84
  92. package/src/routes/storage.ts +7 -2
  93. package/src/routes/tables.ts +42 -29
  94. package/admin-build/_app/immutable/chunks/5PDcRlfX.js +0 -1
  95. package/admin-build/_app/immutable/chunks/qiZXAKh-.js +0 -1
  96. package/admin-build/_app/immutable/entry/start.Mr9mmopc.js +0 -1
  97. package/admin-build/_app/immutable/nodes/21.Ck3_0D2f.js +0 -1
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it } from 'vitest';
2
2
  import { OpenAPIHono, type HonoEnv } from '../lib/hono.js';
3
3
  import { setConfig } from '../lib/do-router.js';
4
4
  import { databaseLiveRoute } from '../routes/database-live.js';
5
+ import { defineConfig } from '@edge-base/shared';
5
6
  import type { Env } from '../types.js';
6
7
 
7
8
  interface MockKVStore {
@@ -52,6 +53,16 @@ describe('database live subscription route', () => {
52
53
  });
53
54
 
54
55
  it('reports connect-check readiness for explicit database params', async () => {
56
+ setConfig(defineConfig({
57
+ databases: {
58
+ shared: {
59
+ tables: {
60
+ posts: { schema: { title: { type: 'string' } } },
61
+ },
62
+ },
63
+ },
64
+ }));
65
+
55
66
  const kv = createMockKV();
56
67
  const app = createApp();
57
68
 
@@ -90,7 +101,78 @@ describe('database live subscription route', () => {
90
101
  });
91
102
  });
92
103
 
104
+ it('rejects structured targets that omit instanceId for dynamic namespaces', async () => {
105
+ setConfig(defineConfig({
106
+ databases: {
107
+ workspace: {
108
+ instance: true,
109
+ tables: {
110
+ posts: { schema: { title: { type: 'string' } } },
111
+ },
112
+ },
113
+ },
114
+ }));
115
+
116
+ const kv = createMockKV();
117
+ const app = createApp();
118
+
119
+ const response = await app.request('/api/db/connect-check?namespace=workspace&table=posts', {
120
+ method: 'GET',
121
+ headers: {
122
+ 'CF-Connecting-IP': '127.0.0.1',
123
+ },
124
+ }, createMockEnv(kv));
125
+
126
+ expect(response.status).toBe(400);
127
+ await expect(response.json()).resolves.toMatchObject({
128
+ ok: false,
129
+ type: 'db_connect_invalid_request',
130
+ message: "instanceId is required for dynamic namespace 'workspace'",
131
+ });
132
+ });
133
+
134
+ it('rejects legacy channels that target dynamic namespaces without an instanceId', async () => {
135
+ setConfig(defineConfig({
136
+ databases: {
137
+ workspace: {
138
+ instance: true,
139
+ tables: {
140
+ posts: { schema: { title: { type: 'string' } } },
141
+ },
142
+ },
143
+ },
144
+ }));
145
+
146
+ const kv = createMockKV();
147
+ const app = createApp();
148
+
149
+ const response = await app.request('/api/db/connect-check?channel=dblive:workspace:posts', {
150
+ method: 'GET',
151
+ headers: {
152
+ 'CF-Connecting-IP': '127.0.0.1',
153
+ },
154
+ }, createMockEnv(kv));
155
+
156
+ expect(response.status).toBe(400);
157
+ await expect(response.json()).resolves.toMatchObject({
158
+ ok: false,
159
+ type: 'db_connect_invalid_request',
160
+ message: "instanceId is required for dynamic namespace 'workspace'",
161
+ });
162
+ });
163
+
93
164
  it('proxies websocket subscribe requests through the database-owned path and releases pending slots', async () => {
165
+ setConfig(defineConfig({
166
+ databases: {
167
+ workspace: {
168
+ instance: true,
169
+ tables: {
170
+ posts: { schema: { title: { type: 'string' } } },
171
+ },
172
+ },
173
+ },
174
+ }));
175
+
94
176
  const kv = createMockKV();
95
177
  const app = createApp();
96
178
  let forwardedUrl = '';
@@ -18,8 +18,12 @@ import {
18
18
  parseConfig,
19
19
  getTablesInNamespace,
20
20
  findTableNamespace,
21
+ formatDbTargetValidationIssue,
21
22
  callDO,
22
23
  callDOByHexId,
24
+ isDynamicDbBlock,
25
+ normalizeDbInstanceId,
26
+ resolveDbTarget,
23
27
  shouldRouteToD1,
24
28
  getD1BindingName,
25
29
  } from '../lib/do-router.js';
@@ -634,6 +638,118 @@ describe('callDOByHexId', () => {
634
638
 
635
639
  // ─── J. shouldRouteToD1 ───────────────────────────────────────────────────────
636
640
 
641
+ describe('isDynamicDbBlock', () => {
642
+ it('returns false for undefined db blocks', () => {
643
+ expect(isDynamicDbBlock()).toBe(false);
644
+ });
645
+
646
+ it('returns false for single-instance db blocks without create/access semantics', () => {
647
+ expect(isDynamicDbBlock({ tables: { posts: {} } } as any)).toBe(false);
648
+ });
649
+
650
+ it('returns true for db blocks with instance routing', () => {
651
+ expect(isDynamicDbBlock({ instance: true, tables: {} } as any)).toBe(true);
652
+ });
653
+
654
+ it('returns true for db blocks with canCreate rules', () => {
655
+ expect(isDynamicDbBlock({
656
+ access: {
657
+ canCreate: () => true,
658
+ },
659
+ tables: {},
660
+ } as any)).toBe(true);
661
+ });
662
+
663
+ it('returns true for db blocks with access rules', () => {
664
+ expect(isDynamicDbBlock({
665
+ access: {
666
+ access: () => true,
667
+ },
668
+ tables: {},
669
+ } as any)).toBe(true);
670
+ });
671
+ });
672
+
673
+ describe('normalizeDbInstanceId', () => {
674
+ it('preserves non-empty ids verbatim', () => {
675
+ expect(normalizeDbInstanceId(' ws-1 ')).toBe(' ws-1 ');
676
+ });
677
+
678
+ it('keeps blank string inputs distinguishable from missing ids', () => {
679
+ expect(normalizeDbInstanceId(' ')).toBe(' ');
680
+ expect(normalizeDbInstanceId(undefined)).toBeUndefined();
681
+ expect(normalizeDbInstanceId(null)).toBeUndefined();
682
+ });
683
+ });
684
+
685
+ describe('resolveDbTarget', () => {
686
+ const config = {
687
+ databases: {
688
+ shared: { tables: { posts: {} } },
689
+ workspace: { instance: true, tables: { members: {} } },
690
+ },
691
+ } as any;
692
+
693
+ it('resolves single-instance namespaces without instance ids', () => {
694
+ expect(resolveDbTarget(config, 'shared', undefined)).toEqual({
695
+ ok: true,
696
+ value: {
697
+ namespace: 'shared',
698
+ instanceId: undefined,
699
+ dbBlock: config.databases.shared,
700
+ dynamic: false,
701
+ },
702
+ });
703
+ });
704
+
705
+ it('rejects instance ids on single-instance namespaces', () => {
706
+ const result = resolveDbTarget(config, 'shared', 'shadow');
707
+ expect(result.ok).toBe(false);
708
+ if (result.ok) return;
709
+ expect(result.issue).toBe('instance_id_not_allowed');
710
+ expect(formatDbTargetValidationIssue(result.issue, 'shared')).toBe(
711
+ "instanceId is not allowed for single-instance namespace 'shared'",
712
+ );
713
+ });
714
+
715
+ it('requires instance ids on dynamic namespaces', () => {
716
+ const result = resolveDbTarget(config, 'workspace', undefined);
717
+ expect(result.ok).toBe(false);
718
+ if (result.ok) return;
719
+ expect(result.issue).toBe('instance_id_required');
720
+ });
721
+
722
+ it('rejects empty instance ids without rewriting non-empty ones', () => {
723
+ const empty = resolveDbTarget(config, 'workspace', ' ');
724
+ expect(empty.ok).toBe(false);
725
+ if (empty.ok) return;
726
+ expect(empty.issue).toBe('instance_id_empty');
727
+ expect(formatDbTargetValidationIssue(empty.issue, 'workspace')).toBe(
728
+ 'instanceId must not be empty',
729
+ );
730
+
731
+ const preserved = resolveDbTarget(config, 'workspace', ' ws-1 ');
732
+ expect(preserved.ok).toBe(true);
733
+ if (!preserved.ok) return;
734
+ expect(preserved.value.instanceId).toBe(' ws-1 ');
735
+ });
736
+
737
+ it('rejects ids containing colons', () => {
738
+ const result = resolveDbTarget(config, 'workspace', 'ws:1');
739
+ expect(result.ok).toBe(false);
740
+ if (result.ok) return;
741
+ expect(result.issue).toBe('instance_id_invalid');
742
+ });
743
+
744
+ it('returns not-found when the namespace is missing', () => {
745
+ const result = resolveDbTarget(config, 'missing', undefined);
746
+ expect(result.ok).toBe(false);
747
+ if (result.ok) return;
748
+ expect(result.status).toBe(404);
749
+ expect(result.issue).toBe('namespace_not_found');
750
+ });
751
+ });
752
+
637
753
  describe('shouldRouteToD1', () => {
638
754
  it('namespace not in config → false', () => {
639
755
  expect(shouldRouteToD1('unknown', {})).toBe(false);
@@ -154,6 +154,13 @@ describe('hookRejectedError', () => {
154
154
  expect(hookRejectedError(original)).toBe(original);
155
155
  });
156
156
 
157
+ it('uses the fallback message and hook prefix for unauthorized non-Error rejections', () => {
158
+ const err = hookRejectedError('plain failure', 'Authentication required', 'beforeSave');
159
+ expect(err.code).toBe(401);
160
+ expect(err.message).toBe("Hook 'beforeSave' rejected: Authentication required");
161
+ expect(err.slug).toBe('hook-rejected');
162
+ });
163
+
157
164
  it('maps ownership denial messages to 403', () => {
158
165
  const err = hookRejectedError(new Error('Only owners can update this record.'));
159
166
  expect(err.code).toBe(403);
@@ -171,6 +178,18 @@ describe('hookRejectedError', () => {
171
178
  expect(err.code).toBe(409);
172
179
  });
173
180
 
181
+ it('maps not-found style messages to 404', () => {
182
+ const err = hookRejectedError(new Error('Unknown project id.'));
183
+ expect(err.code).toBe(404);
184
+ expect(err.message).toBe('Unknown project id.');
185
+ });
186
+
187
+ it('maps rate-limit style messages to 429', () => {
188
+ const err = hookRejectedError(new Error('Too many uploads, throttled.'));
189
+ expect(err.code).toBe(429);
190
+ expect(err.slug).toBe('hook-rejected');
191
+ });
192
+
174
193
  it('falls back to validation errors for unknown hook failures', () => {
175
194
  const err = hookRejectedError(new Error('Custom hook failure.'));
176
195
  expect(err.code).toBe(400);
@@ -179,6 +198,11 @@ describe('hookRejectedError', () => {
179
198
  });
180
199
 
181
200
  describe('normalizeDatabaseError', () => {
201
+ it('passes EdgeBaseError instances through untouched', () => {
202
+ const original = validationError('Already normalized');
203
+ expect(normalizeDatabaseError(original)).toBe(original);
204
+ });
205
+
182
206
  it('maps foreign key failures to validation errors', () => {
183
207
  const err = normalizeDatabaseError(new Error('D1_ERROR: FOREIGN KEY constraint failed: SQLITE_CONSTRAINT'));
184
208
  expect(err).toBeInstanceOf(EdgeBaseError);
@@ -187,6 +211,13 @@ describe('normalizeDatabaseError', () => {
187
211
  expect(err?.slug).toBe('foreign-key-failed');
188
212
  });
189
213
 
214
+ it('maps foreign key failures without a detected column to the generic message', () => {
215
+ const err = normalizeDatabaseError('FOREIGN KEY constraint failed');
216
+ expect(err).toBeInstanceOf(EdgeBaseError);
217
+ expect(err?.code).toBe(400);
218
+ expect(err?.message).toContain('Check that all foreign key references');
219
+ });
220
+
190
221
  it('maps foreign key failures from cross-realm error-like objects', () => {
191
222
  const err = normalizeDatabaseError({
192
223
  message: 'D1_ERROR: FOREIGN KEY constraint failed: SQLITE_CONSTRAINT',
@@ -204,6 +235,38 @@ describe('normalizeDatabaseError', () => {
204
235
  expect(err?.slug).toBe('record-already-exists');
205
236
  });
206
237
 
238
+ it('maps unique constraint failures without a parsed column to a generic conflict message', () => {
239
+ const err = normalizeDatabaseError('UNIQUE constraint failed');
240
+ expect(err).toBeInstanceOf(EdgeBaseError);
241
+ expect(err?.code).toBe(409);
242
+ expect(err?.message).toBe('Record already exists. A unique constraint was violated.');
243
+ });
244
+
245
+ it('maps not-null constraint failures to validation errors', () => {
246
+ const err = normalizeDatabaseError(new Error('NOT NULL constraint failed: users.email'));
247
+ expect(err).toBeInstanceOf(EdgeBaseError);
248
+ expect(err?.code).toBe(400);
249
+ expect(err?.message).toContain("'email'");
250
+ expect(err?.slug).toBe('constraint-failed');
251
+ });
252
+
253
+ it('maps cause-only check constraint failures to a generic validation error', () => {
254
+ const err = normalizeDatabaseError({
255
+ message: 'outer wrapper',
256
+ cause: {
257
+ message: 'check constraint failed',
258
+ },
259
+ });
260
+ expect(err).toBeInstanceOf(EdgeBaseError);
261
+ expect(err?.code).toBe(400);
262
+ expect(err?.message).toBe('Request violates a database constraint. Ensure all required fields are provided.');
263
+ expect(err?.slug).toBe('constraint-failed');
264
+ });
265
+
266
+ it('returns null for blank string inputs', () => {
267
+ expect(normalizeDatabaseError(' ')).toBeNull();
268
+ });
269
+
207
270
  it('returns null for unrelated runtime errors', () => {
208
271
  expect(normalizeDatabaseError(new Error('socket hang up'))).toBeNull();
209
272
  });