@edge-base/server 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin-build/_app/immutable/chunks/{C85dMlzL.js → 5RQRbp5q.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B8DT4fss.js → BME_U9TJ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BaCHY17I.js → BYI6CUvd.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BWyDPAjM.js → BgDzp0i0.js} +1 -1
- package/admin-build/_app/immutable/chunks/{c5iKSdWY.js → BjWZuf8W.js} +1 -1
- package/admin-build/_app/immutable/chunks/{g3ZZdY-r.js → C6lpZLE2.js} +1 -1
- package/admin-build/_app/immutable/chunks/{C-DsDCNG.js → D5GswVnI.js} +3 -3
- package/admin-build/_app/immutable/chunks/DBsVqhuh.js +1 -0
- package/admin-build/_app/immutable/chunks/{BEYYl662.js → DYaCRWMA.js} +1 -1
- package/admin-build/_app/immutable/chunks/D__dwMuW.js +1 -0
- package/admin-build/_app/immutable/chunks/{4vlsb8ej.js → Dj-E9-FO.js} +1 -1
- package/admin-build/_app/immutable/chunks/{kiJ6KthZ.js → Dj0QUuOf.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BKXmgPq4.js → XQM1k9PM.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CTngeX8H.js → fYEKMQ-Z.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CPdXvRUb.js → g_-Kpxu3.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DzXaj-Ja.js → wCNueVYy.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.BZxfavhF.js → app.C8ylfBe6.js} +2 -2
- package/admin-build/_app/immutable/entry/start.CtsqDyfj.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.DlsaydXO.js → 0.CJJ6HZbp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.D2NWN5eG.js → 1.B4sI5cB4.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.EMDaN3nw.js → 10.D6hvCer6.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.BasqQ_o9.js → 11.Dx7b8aQ5.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.DO31Ljs7.js → 12.Bqmy5KIF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.DhyAy-GZ.js → 13.CC6KpXgS.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.CLecGWc4.js → 14.yCo1Ix8E.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.B9kp3W4e.js → 15.co0UfPlh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.Pu_8T3RI.js → 16.D0xkPUBW.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.DX4z43t6.js → 17.CebNqPeh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.BKsSaxrr.js → 18.JUoLOZxh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.DXNF1htN.js → 19.ND8kmQJe.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.VRVb0wee.js → 20.DYb-q3W8.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.cz3IN9Cc.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.DqZf4CtH.js → 22.UOzm8WYV.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.DtyxMiQG.js → 23.BLgq21om.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.CloWNmTd.js → 24.DN9usmUs.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.CnZWMq7_.js → 25.BddRfAyE.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.DrV7XOmf.js → 26.Dl6XHIeT.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.DV8L32OF.js → 27.D0iNwALG.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.Stil2D4u.js → 28.9dKQmdGi.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.Zsm1e5Dc.js → 29.wXzfJUXp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.CKoj2vNz.js → 3.z8ut3jS-.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.Ni0k5bER.js → 30.BtZETNsL.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.mnqj9EbV.js → 31.CYonj2Jh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.B_-z9AzT.js → 4.COtDPQ9b.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.yiZ72j4k.js → 5.CTRCeIhp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.BqykybBG.js → 6.ChHi3QkR.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.BDAHlhsF.js → 7.CCMtr6Ac.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.D8Xvy0lH.js → 8.DpWJ-X_-.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.Dddmd7_F.js → 9.DOkvfmir.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/package.json +3 -3
- package/src/__tests__/admin-data-routes.test.ts +29 -0
- package/src/__tests__/d1-live-broadcast-verification.test.ts +271 -0
- package/src/__tests__/database-do-route-validation.test.ts +105 -0
- package/src/__tests__/database-live-do.test.ts +50 -0
- package/src/__tests__/database-live-emitter.test.ts +116 -1
- package/src/__tests__/database-live-route.test.ts +82 -0
- package/src/__tests__/do-router.test.ts +116 -0
- package/src/__tests__/error-format.test.ts +63 -0
- package/src/__tests__/functions-context.test.ts +674 -33
- package/src/__tests__/functions-d1-proxy.test.ts +54 -0
- package/src/__tests__/plugin-migration-routing.test.ts +32 -0
- package/src/__tests__/postgres-field-ops-compat.test.ts +110 -0
- package/src/__tests__/provider-aware-sql.test.ts +163 -0
- package/src/__tests__/room-auth-state-loss.test.ts +124 -0
- package/src/__tests__/runtime-surface-accounting.test.ts +0 -4
- package/src/__tests__/scheduled.test.ts +55 -0
- package/src/__tests__/service-key-db-proxy.test.ts +122 -1
- package/src/__tests__/sql-route.test.ts +252 -75
- package/src/__tests__/table-hook-runtime.test.ts +137 -0
- package/src/durable-objects/database-do.ts +36 -45
- package/src/durable-objects/database-live-do.ts +46 -1
- package/src/durable-objects/room-runtime-base.ts +26 -2
- package/src/durable-objects/rooms-do.ts +1 -1
- package/src/index.ts +12 -6
- package/src/lib/admin-db-target.ts +30 -74
- package/src/lib/d1-handler.ts +55 -35
- package/src/lib/database-live-emitter.ts +57 -16
- package/src/lib/do-router.ts +135 -3
- package/src/lib/functions.ts +215 -143
- package/src/lib/internal-transport.ts +28 -12
- package/src/lib/plugin-migration-routing.ts +28 -0
- package/src/lib/plugin-migrations.ts +38 -38
- package/src/lib/postgres-handler.ts +51 -31
- package/src/lib/provider-aware-sql.ts +831 -0
- package/src/lib/table-hook-runtime.ts +62 -0
- package/src/routes/admin.ts +41 -41
- package/src/routes/auth.ts +7 -2
- package/src/routes/database-live.ts +110 -12
- package/src/routes/sql.ts +64 -84
- package/src/routes/storage.ts +7 -2
- package/src/routes/tables.ts +42 -29
- package/admin-build/_app/immutable/chunks/5PDcRlfX.js +0 -1
- package/admin-build/_app/immutable/chunks/qiZXAKh-.js +0 -1
- package/admin-build/_app/immutable/entry/start.Mr9mmopc.js +0 -1
- package/admin-build/_app/immutable/nodes/21.Ck3_0D2f.js +0 -1
|
@@ -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
|
});
|