@edge-base/server 0.2.9 → 0.3.2
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/BENT96gS.js +1 -0
- package/admin-build/_app/immutable/chunks/{BfhqI0W8.js → BR-X1vqg.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DncT8xH5.js → BYTXdmFz.js} +1 -1
- package/admin-build/_app/immutable/chunks/{YGUb3X-Q.js → C-_7bS5w.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DUhE3Ynq.js → CCB6XzFZ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{39YMyjjq.js → CWTeyOdB.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DllVADtt.js → CX5CmbTG.js} +1 -1
- package/admin-build/_app/immutable/chunks/{0M5txo3l.js → CdgSuiqt.js} +3 -3
- package/admin-build/_app/immutable/chunks/{BN-pHKcH.js → Cuny2nPU.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CPLcjNR9.js → D15DP4DQ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DBkor_jM.js → DAzRCtch.js} +1 -1
- package/admin-build/_app/immutable/chunks/DYF4byEY.js +1 -0
- package/admin-build/_app/immutable/chunks/{DBniE7GN.js → Dcey5WNo.js} +1 -1
- package/admin-build/_app/immutable/chunks/{C5nMidnK.js → HK54RDdv.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CI1lKBjB.js → gUtOgqgM.js} +1 -1
- package/admin-build/_app/immutable/chunks/{_xa30BOD.js → pnqPwnFK.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.BW-Ac3jY.js → app.DwslmBQo.js} +2 -2
- package/admin-build/_app/immutable/entry/start.Bi51W3Uj.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.CRSQPtos.js → 0.SSaZIC3t.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.HmStNbZP.js → 1.CqSv9lq5.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.BVlz13nr.js → 10.BoQz_717.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.VwQdbWwH.js → 11.B-yS_gft.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.BFAAgi1f.js → 12.WsVtLoVx.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.DDdauC84.js → 13.TbqRQJKh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.D_rXaafJ.js → 14.cldePwj3.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.rfFjjiZc.js → 15.Bec037-f.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.DDEpGJ4Z.js → 16.Dd0yTYhN.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.ChQWNtRN.js → 17.CoiqH0lF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.Rir_JwlF.js → 18.BDXxM1Qi.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.CF76nxk1.js → 19.lo9mTPiR.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.JQer3KPM.js → 20.B7kbGqh_.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.BGm1TcfO.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.DUw7X6z_.js → 22.VOtCuTkf.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.DS3svA-a.js → 23.C2Uxx-kO.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.BYOsQM6B.js → 24.BxdEwsS3.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.n0pyDBRv.js → 25.CVu2qoCv.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.C-Oo8sYK.js → 26.DqYlpX7Z.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.BfxbFNkk.js → 27.emgbpP8y.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.DyHSN1q7.js → 28.BVPSPK68.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.CUAmZbCc.js → 29.BsDQQ16z.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.KQm4VJJg.js → 3.uc8joqGb.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.CQdPre5F.js → 30.D4CYqQrE.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.BP6raetq.js → 31.DvUuXrVU.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.Glf2jJHB.js → 4.BqTZ0Uoq.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.BdFtj_-X.js → 5.DAO3--6W.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.JfnwWq8s.js → 6.CBbGl_Wo.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.C-rw9qDh.js → 7.s-I8cKAh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.CA7r1Sjs.js → 8.Ct6B-y3f.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.B2jgJDkH.js → 9.Dok_ZoeZ.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/openapi.json +498 -3
- package/package.json +3 -3
- package/src/__tests__/auth-token.test.ts +142 -0
- package/src/__tests__/email-provider.test.ts +82 -0
- package/src/__tests__/error-format.test.ts +21 -0
- package/src/__tests__/functions-context.test.ts +87 -0
- package/src/__tests__/functions-d1-proxy.test.ts +70 -0
- package/src/__tests__/functions-route.test.ts +15 -0
- package/src/__tests__/postgres-api-consistency.test.ts +240 -0
- package/src/__tests__/postgres-batch-compat.test.ts +33 -27
- package/src/__tests__/postgres-batch.test.ts +394 -0
- package/src/__tests__/postgres-field-ops-compat.test.ts +2 -1
- package/src/__tests__/postgres-transact.test.ts +148 -0
- package/src/__tests__/room-access-policy.test.ts +26 -0
- package/src/__tests__/room-handler-context.test.ts +2 -0
- package/src/__tests__/smoke-skip-report.test.ts +1 -1
- package/src/durable-objects/database-do.ts +455 -15
- package/src/durable-objects/room-runtime-base.ts +16 -5
- package/src/durable-objects/rooms-do.ts +4 -1
- package/src/lib/auth-d1-service.ts +37 -7
- package/src/lib/auth-token.ts +66 -0
- package/src/lib/d1-handler.ts +416 -50
- package/src/lib/email-provider.ts +115 -5
- package/src/lib/errors.ts +8 -4
- package/src/lib/functions.ts +91 -1
- package/src/lib/internal-transport.ts +25 -1
- package/src/lib/jwt.ts +2 -0
- package/src/lib/postgres-handler.ts +562 -84
- package/src/routes/admin.ts +8 -1
- package/src/routes/auth.ts +169 -65
- package/src/routes/room.ts +36 -2
- package/src/routes/storage.ts +446 -62
- package/src/routes/tables.ts +140 -0
- package/src/types.ts +10 -0
- package/admin-build/_app/immutable/chunks/B0zJ5_Wk.js +0 -1
- package/admin-build/_app/immutable/chunks/DZo4s3wn.js +0 -1
- package/admin-build/_app/immutable/entry/start.Bcf0SjPV.js +0 -1
- package/admin-build/_app/immutable/nodes/21.CzGYxjpu.js +0 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL handler ↔ D1/DO contract parity (single-record + search):
|
|
3
|
+
* - GET /search without a term → 200 { items: [] } (was 400)
|
|
4
|
+
* - PATCH {id} with an empty body → 200 existing record (was 400)
|
|
5
|
+
* - DELETE {id} → { deleted: true } (was { success: true, deleted: <row> })
|
|
6
|
+
* - validation failures carry only { code, message, data } (no legacy `errors`)
|
|
7
|
+
* - 403 rule-rejection wording matches the D1 handler
|
|
8
|
+
*
|
|
9
|
+
* Uses a mocked executor (same pattern as postgres-batch.test.ts).
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, it, vi, afterEach } from 'vitest';
|
|
12
|
+
import { defineConfig } from '@edge-base/shared';
|
|
13
|
+
import { buildInternalHandlerContext } from '../lib/internal-request.js';
|
|
14
|
+
import type { Env } from '../types.js';
|
|
15
|
+
|
|
16
|
+
const CONFIG = defineConfig({
|
|
17
|
+
release: true,
|
|
18
|
+
databases: {
|
|
19
|
+
shared: {
|
|
20
|
+
provider: 'postgres',
|
|
21
|
+
connectionString: 'DB_POSTGRES_SHARED_URL',
|
|
22
|
+
tables: {
|
|
23
|
+
api_docs: {
|
|
24
|
+
schema: {
|
|
25
|
+
title: { type: 'string', required: true },
|
|
26
|
+
status: { type: 'string' },
|
|
27
|
+
},
|
|
28
|
+
access: {
|
|
29
|
+
read: () => true,
|
|
30
|
+
insert: () => true,
|
|
31
|
+
update: () => true,
|
|
32
|
+
delete: () => true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
// Auto timestamps disabled so an empty update body yields a genuinely
|
|
36
|
+
// empty change set (exercises the no-op early-return path).
|
|
37
|
+
api_notes: {
|
|
38
|
+
schema: {
|
|
39
|
+
title: { type: 'string', required: true },
|
|
40
|
+
createdAt: false,
|
|
41
|
+
updatedAt: false,
|
|
42
|
+
},
|
|
43
|
+
access: {
|
|
44
|
+
read: () => true,
|
|
45
|
+
insert: () => true,
|
|
46
|
+
update: () => true,
|
|
47
|
+
delete: () => true,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function makeEnv(): Env {
|
|
56
|
+
return {
|
|
57
|
+
EDGEBASE_CONFIG: CONFIG,
|
|
58
|
+
DB_POSTGRES_SHARED_URL: 'postgres://edgebase:test@localhost/shared',
|
|
59
|
+
} as unknown as Env;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mockExecutorModule(
|
|
63
|
+
handler: (sql: string, params: unknown[]) => { rows: Record<string, unknown>[]; rowCount: number },
|
|
64
|
+
) {
|
|
65
|
+
const calls: Array<{ sql: string; params: unknown[] }> = [];
|
|
66
|
+
vi.doMock('../lib/postgres-executor.js', () => ({
|
|
67
|
+
executePostgresQuery: vi.fn(),
|
|
68
|
+
ensureLocalDevPostgresSchema: vi.fn().mockResolvedValue(undefined),
|
|
69
|
+
withPostgresConnection: vi.fn(async (_cs: string, fn: (q: unknown) => Promise<unknown>) =>
|
|
70
|
+
fn(async (sql: string, params: unknown[] = []) => {
|
|
71
|
+
calls.push({ sql, params });
|
|
72
|
+
return handler(sql, params);
|
|
73
|
+
})),
|
|
74
|
+
getLocalDevPostgresExecOptions: vi.fn(() => undefined),
|
|
75
|
+
getProviderBindingName: () => 'DB_POSTGRES_SHARED',
|
|
76
|
+
}));
|
|
77
|
+
vi.doMock('../lib/postgres-schema-init.js', () => ({
|
|
78
|
+
ensurePgSchema: vi.fn().mockResolvedValue(undefined),
|
|
79
|
+
}));
|
|
80
|
+
return calls;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function callPg(
|
|
84
|
+
method: string,
|
|
85
|
+
path: string,
|
|
86
|
+
{ body, search, query, table = 'api_docs' }:
|
|
87
|
+
{ body?: unknown; search?: string; query?: string; table?: string } = {},
|
|
88
|
+
) {
|
|
89
|
+
const { handlePgRequest } = await import('../lib/postgres-handler.js');
|
|
90
|
+
const qs = search !== undefined
|
|
91
|
+
? `?search=${encodeURIComponent(search)}`
|
|
92
|
+
: query !== undefined ? `?${query}` : '';
|
|
93
|
+
const url = `http://internal/api/db/shared${path}${qs}`;
|
|
94
|
+
const ctx = buildInternalHandlerContext({
|
|
95
|
+
env: makeEnv(),
|
|
96
|
+
request: new Request(url, {
|
|
97
|
+
method,
|
|
98
|
+
headers: { 'Content-Type': 'application/json', 'X-Is-Service-Key': 'true' },
|
|
99
|
+
}),
|
|
100
|
+
body: body as Record<string, unknown>,
|
|
101
|
+
});
|
|
102
|
+
return handlePgRequest(ctx as never, 'shared', table, path);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
describe('postgres API consistency (D1/DO parity)', () => {
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
vi.resetModules();
|
|
108
|
+
vi.clearAllMocks();
|
|
109
|
+
vi.unstubAllGlobals();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('returns 200 { items: [] } when the search term is missing (no 400)', async () => {
|
|
113
|
+
const calls = mockExecutorModule(() => ({ rows: [], rowCount: 0 }));
|
|
114
|
+
const response = await callPg('GET', '/tables/api_docs/search');
|
|
115
|
+
expect(response.status).toBe(200);
|
|
116
|
+
expect(await response.json()).toEqual({ items: [] });
|
|
117
|
+
// Early return — no search query is issued.
|
|
118
|
+
expect(calls.some((c) => /ILIKE/i.test(c.sql))).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('runs an ILIKE search over text fields when a term is provided', async () => {
|
|
122
|
+
const hit = { id: 'doc-1', title: 'hello world', status: 'open' };
|
|
123
|
+
const calls = mockExecutorModule((sql) => {
|
|
124
|
+
if (/ILIKE/i.test(sql) && !/COUNT/i.test(sql)) return { rows: [hit], rowCount: 1 };
|
|
125
|
+
if (/COUNT/i.test(sql)) return { rows: [{ total: 1 }], rowCount: 1 };
|
|
126
|
+
return { rows: [], rowCount: 0 };
|
|
127
|
+
});
|
|
128
|
+
const response = await callPg('GET', '/tables/api_docs/search', { search: 'hello' });
|
|
129
|
+
expect(response.status).toBe(200);
|
|
130
|
+
const json = (await response.json()) as { items: unknown[]; total: number };
|
|
131
|
+
expect(json.items).toHaveLength(1);
|
|
132
|
+
expect(json.total).toBe(1);
|
|
133
|
+
// The search scans the schema's text columns (title, status), not just id.
|
|
134
|
+
const searchSql = calls.find((c) => /ILIKE/i.test(c.sql))?.sql ?? '';
|
|
135
|
+
expect(searchSql).toMatch(/"title"/);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('returns the existing record with 200 for an empty update body (no 400)', async () => {
|
|
139
|
+
const existing = { id: 'note-1', title: 'Kept' };
|
|
140
|
+
const calls = mockExecutorModule((sql) => {
|
|
141
|
+
if (sql.startsWith('SELECT')) return { rows: [existing], rowCount: 1 };
|
|
142
|
+
return { rows: [], rowCount: 0 };
|
|
143
|
+
});
|
|
144
|
+
const response = await callPg('PATCH', '/tables/api_notes/note-1', { body: {}, table: 'api_notes' });
|
|
145
|
+
expect(response.status).toBe(200);
|
|
146
|
+
const json = (await response.json()) as Record<string, unknown>;
|
|
147
|
+
expect(json.id).toBe('note-1');
|
|
148
|
+
expect(json.title).toBe('Kept');
|
|
149
|
+
// No UPDATE statement is executed for a no-op update.
|
|
150
|
+
expect(calls.some((c) => c.sql.startsWith('UPDATE'))).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('returns { deleted: true } from a single-record delete', async () => {
|
|
154
|
+
const existing = { id: 'doc-1', title: 'Bye', status: 'open' };
|
|
155
|
+
mockExecutorModule((sql) => {
|
|
156
|
+
if (sql.startsWith('SELECT')) return { rows: [existing], rowCount: 1 };
|
|
157
|
+
if (sql.startsWith('DELETE')) return { rows: [existing], rowCount: 1 };
|
|
158
|
+
return { rows: [], rowCount: 0 };
|
|
159
|
+
});
|
|
160
|
+
const response = await callPg('DELETE', '/tables/api_docs/doc-1');
|
|
161
|
+
expect(response.status).toBe(200);
|
|
162
|
+
expect(await response.json()).toEqual({ deleted: true });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('omits the legacy `errors` field from validation failures', async () => {
|
|
166
|
+
mockExecutorModule(() => ({ rows: [], rowCount: 0 }));
|
|
167
|
+
// Missing required `title`.
|
|
168
|
+
const response = await callPg('POST', '/tables/api_docs', { body: { status: 'open' } });
|
|
169
|
+
expect(response.status).toBe(400);
|
|
170
|
+
const json = (await response.json()) as Record<string, unknown>;
|
|
171
|
+
expect(json.data).toBeDefined();
|
|
172
|
+
expect(json).not.toHaveProperty('errors');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('lists records with a total count', async () => {
|
|
176
|
+
mockExecutorModule((sql) => {
|
|
177
|
+
if (/COUNT/i.test(sql)) return { rows: [{ total: 2 }], rowCount: 1 };
|
|
178
|
+
if (sql.startsWith('SELECT')) {
|
|
179
|
+
return { rows: [{ id: 'a', title: 'A' }, { id: 'b', title: 'B' }], rowCount: 2 };
|
|
180
|
+
}
|
|
181
|
+
return { rows: [], rowCount: 0 };
|
|
182
|
+
});
|
|
183
|
+
const response = await callPg('GET', '/tables/api_docs');
|
|
184
|
+
expect(response.status).toBe(200);
|
|
185
|
+
const json = (await response.json()) as { items: unknown[]; total: number };
|
|
186
|
+
expect(json.items).toHaveLength(2);
|
|
187
|
+
expect(json.total).toBe(2);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('skips the COUNT query when includeTotal=0 (total is null)', async () => {
|
|
191
|
+
const calls = mockExecutorModule((sql) => {
|
|
192
|
+
if (sql.startsWith('SELECT')) return { rows: [{ id: 'a', title: 'A' }], rowCount: 1 };
|
|
193
|
+
return { rows: [], rowCount: 0 };
|
|
194
|
+
});
|
|
195
|
+
const response = await callPg('GET', '/tables/api_docs', { query: 'includeTotal=0' });
|
|
196
|
+
expect(response.status).toBe(200);
|
|
197
|
+
const json = (await response.json()) as { items: unknown[]; total: number | null };
|
|
198
|
+
expect(json.items).toHaveLength(1);
|
|
199
|
+
expect(json.total).toBeNull();
|
|
200
|
+
expect(calls.some((c) => /COUNT/i.test(c.sql))).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('gets a single record by id', async () => {
|
|
204
|
+
mockExecutorModule((sql) => {
|
|
205
|
+
if (sql.startsWith('SELECT')) return { rows: [{ id: 'doc-1', title: 'Hi' }], rowCount: 1 };
|
|
206
|
+
return { rows: [], rowCount: 0 };
|
|
207
|
+
});
|
|
208
|
+
const response = await callPg('GET', '/tables/api_docs/doc-1');
|
|
209
|
+
expect(response.status).toBe(200);
|
|
210
|
+
expect((await response.json() as { id: string }).id).toBe('doc-1');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('returns 404 for a missing record', async () => {
|
|
214
|
+
mockExecutorModule(() => ({ rows: [], rowCount: 0 }));
|
|
215
|
+
const response = await callPg('GET', '/tables/api_docs/nope');
|
|
216
|
+
expect(response.status).toBe(404);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('counts records', async () => {
|
|
220
|
+
mockExecutorModule((sql) => {
|
|
221
|
+
if (/COUNT/i.test(sql)) return { rows: [{ total: 7 }], rowCount: 1 };
|
|
222
|
+
return { rows: [], rowCount: 0 };
|
|
223
|
+
});
|
|
224
|
+
const response = await callPg('GET', '/tables/api_docs/count');
|
|
225
|
+
expect(response.status).toBe(200);
|
|
226
|
+
expect((await response.json() as { total: number }).total).toBe(7);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('inserts a record and returns it', async () => {
|
|
230
|
+
mockExecutorModule((sql) => {
|
|
231
|
+
if (sql.startsWith('INSERT')) {
|
|
232
|
+
return { rows: [{ id: 'new-1', title: 'Fresh', status: 'open' }], rowCount: 1 };
|
|
233
|
+
}
|
|
234
|
+
return { rows: [], rowCount: 0 };
|
|
235
|
+
});
|
|
236
|
+
const response = await callPg('POST', '/tables/api_docs', { body: { title: 'Fresh' } });
|
|
237
|
+
expect([200, 201]).toContain(response.status);
|
|
238
|
+
expect((await response.json() as { title: string }).title).toBe('Fresh');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -11,31 +11,33 @@ describe('postgres batch compatibility', () => {
|
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
it('accepts D1-style batch inserts for upsertMany and returns inserted rows', async () => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
rowCount: 1
|
|
38
|
-
}
|
|
14
|
+
let insertCount = 0;
|
|
15
|
+
const insertRows = [
|
|
16
|
+
{
|
|
17
|
+
id: 'probe-1',
|
|
18
|
+
probeKey: 'probe-a',
|
|
19
|
+
phase: 'batch',
|
|
20
|
+
sourceProvider: 'neon',
|
|
21
|
+
targetProvider: 'neon',
|
|
22
|
+
checksum: 'checksum-a',
|
|
23
|
+
verified: false,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'probe-2',
|
|
27
|
+
probeKey: 'probe-b',
|
|
28
|
+
phase: 'batch',
|
|
29
|
+
sourceProvider: 'neon',
|
|
30
|
+
targetProvider: 'neon',
|
|
31
|
+
checksum: 'checksum-b',
|
|
32
|
+
verified: false,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
const executePostgresQuery = vi.fn(async (_cs: string, sql: string, _params?: unknown[]) => {
|
|
36
|
+
if (sql.startsWith('INSERT INTO')) {
|
|
37
|
+
return { rows: [insertRows[insertCount++]], rowCount: 1 };
|
|
38
|
+
}
|
|
39
|
+
return { rows: [], rowCount: 0 };
|
|
40
|
+
});
|
|
39
41
|
|
|
40
42
|
vi.doMock('../lib/postgres-executor.js', () => ({
|
|
41
43
|
executePostgresQuery,
|
|
@@ -126,8 +128,12 @@ describe('postgres batch compatibility', () => {
|
|
|
126
128
|
expect(response.status).toBe(200);
|
|
127
129
|
expect(json.inserted).toHaveLength(2);
|
|
128
130
|
expect(json.items).toHaveLength(2);
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
+
// Batch inserts are wrapped in a single BEGIN/COMMIT transaction.
|
|
132
|
+
const sqls = executePostgresQuery.mock.calls.map((call) => call[1] as string);
|
|
133
|
+
expect(sqls[0]).toBe('BEGIN');
|
|
134
|
+
expect(sqls[sqls.length - 1]).toBe('COMMIT');
|
|
135
|
+
expect(sqls.filter((sql) => sql.startsWith('INSERT INTO'))).toHaveLength(2);
|
|
136
|
+
const firstSql = sqls.find((sql) => sql.startsWith('INSERT INTO'))!;
|
|
131
137
|
expect(firstSql).toContain('ON CONFLICT ("probeKey") DO UPDATE');
|
|
132
138
|
});
|
|
133
139
|
});
|