@zveltio/sdk 3.0.0-beta.5 → 3.0.0-beta.70
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/dist/client/Auth.d.ts.map +1 -1
- package/dist/client/Auth.js +3 -0
- package/dist/client/QueryBuilder.d.ts.map +1 -1
- package/dist/client/QueryBuilder.js +4 -0
- package/dist/client/RealtimeClient.d.ts +19 -1
- package/dist/client/RealtimeClient.d.ts.map +1 -1
- package/dist/client/RealtimeClient.js +65 -12
- package/dist/client/ZveltioClient.d.ts.map +1 -1
- package/dist/client/ZveltioClient.js +12 -1
- package/dist/client.d.ts +28 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +41 -14
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +21 -3
- package/dist/crdt.d.ts.map +1 -1
- package/dist/crdt.js +11 -2
- package/dist/ddl/index.d.ts.map +1 -1
- package/dist/errors.d.ts +58 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +124 -0
- package/dist/extension/index.d.ts +459 -29
- package/dist/extension/index.d.ts.map +1 -1
- package/dist/extension/index.js +3 -0
- package/dist/extension/jsonb.d.ts +62 -0
- package/dist/extension/jsonb.d.ts.map +1 -0
- package/dist/extension/jsonb.js +64 -0
- package/dist/extension/multipart.d.ts +58 -0
- package/dist/extension/multipart.d.ts.map +1 -0
- package/dist/extension/multipart.js +61 -0
- package/dist/extension/numeric.d.ts +94 -0
- package/dist/extension/numeric.d.ts.map +1 -0
- package/dist/extension/numeric.js +162 -0
- package/dist/extension/permission-gate.d.ts +15 -0
- package/dist/extension/permission-gate.d.ts.map +1 -1
- package/dist/extension/permission-gate.js +33 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/local-store.d.ts +5 -0
- package/dist/local-store.d.ts.map +1 -1
- package/dist/local-store.js +62 -10
- package/dist/offline/index.d.ts +3 -3
- package/dist/offline/index.js +51 -15
- package/dist/publish/index.d.ts.map +1 -1
- package/dist/publish/index.js +6 -1
- package/dist/realtime.d.ts +18 -1
- package/dist/realtime.d.ts.map +1 -1
- package/dist/realtime.js +34 -3
- package/dist/rpc/index.d.ts.map +1 -1
- package/dist/rpc/index.js +2 -0
- package/dist/schema-codegen.d.ts +3 -1
- package/dist/schema-codegen.d.ts.map +1 -1
- package/dist/schema-codegen.js +41 -7
- package/dist/schema-watcher.d.ts +10 -2
- package/dist/schema-watcher.d.ts.map +1 -1
- package/dist/schema-watcher.js +30 -10
- package/dist/studio/index.d.ts +1 -0
- package/dist/studio/index.d.ts.map +1 -1
- package/dist/studio/index.js +3 -0
- package/dist/studio/slot-owner-registry.d.ts +13 -0
- package/dist/studio/slot-owner-registry.d.ts.map +1 -0
- package/dist/studio/slot-owner-registry.js +21 -0
- package/dist/svelte.d.ts +1 -1
- package/dist/svelte.d.ts.map +1 -1
- package/dist/svelte.js +4 -2
- package/dist/sync-manager.d.ts +30 -4
- package/dist/sync-manager.d.ts.map +1 -1
- package/dist/sync-manager.js +101 -15
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +23 -1
- package/dist/testing/with-test-db.d.ts.map +1 -1
- package/dist/testing/with-test-db.js +21 -14
- package/dist/tests/client.test.d.ts +2 -0
- package/dist/tests/client.test.d.ts.map +1 -0
- package/dist/tests/client.test.js +61 -0
- package/dist/tests/crdt-orset.test.d.ts +2 -0
- package/dist/tests/crdt-orset.test.d.ts.map +1 -0
- package/dist/tests/crdt-orset.test.js +23 -0
- package/dist/tests/local-store.test.js +44 -0
- package/dist/tests/multipart.test.d.ts +2 -0
- package/dist/tests/multipart.test.d.ts.map +1 -0
- package/dist/tests/multipart.test.js +37 -0
- package/dist/tests/offline-provider-crdt.test.d.ts +2 -0
- package/dist/tests/offline-provider-crdt.test.d.ts.map +1 -0
- package/dist/tests/offline-provider-crdt.test.js +119 -0
- package/dist/tests/realtime.test.d.ts +2 -0
- package/dist/tests/realtime.test.d.ts.map +1 -0
- package/dist/tests/realtime.test.js +284 -0
- package/dist/tests/sync-manager.test.d.ts +2 -0
- package/dist/tests/sync-manager.test.d.ts.map +1 -0
- package/dist/tests/sync-manager.test.js +65 -0
- package/dist/tests/with-test-db.test.d.ts +2 -0
- package/dist/tests/with-test-db.test.d.ts.map +1 -0
- package/dist/tests/with-test-db.test.js +57 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/validate/index.d.ts +49 -0
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +503 -7
- package/dist/validate/shared-message-keys.d.ts +16 -0
- package/dist/validate/shared-message-keys.d.ts.map +1 -0
- package/dist/validate/shared-message-keys.js +227 -0
- package/package.json +7 -7
- package/dist/generate-types.d.ts +0 -37
- package/dist/generate-types.d.ts.map +0 -1
- package/dist/generate-types.js +0 -157
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, jest, spyOn } from 'bun:test';
|
|
2
|
+
import { rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { RealtimeClient } from '../client/RealtimeClient.js';
|
|
5
|
+
import { ZveltioClient } from '../client/ZveltioClient.js';
|
|
6
|
+
import { ZveltioRealtime } from '../realtime.js';
|
|
7
|
+
import { watchSchema } from '../schema-watcher.js';
|
|
8
|
+
const RealWebSocket = globalThis.WebSocket;
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
globalThis.WebSocket = RealWebSocket;
|
|
11
|
+
});
|
|
12
|
+
/** Records the constructor arguments instead of dialling out. */
|
|
13
|
+
function captureSockets() {
|
|
14
|
+
const calls = [];
|
|
15
|
+
globalThis.WebSocket = class {
|
|
16
|
+
static OPEN = 1;
|
|
17
|
+
readyState = 0;
|
|
18
|
+
constructor(...args) {
|
|
19
|
+
calls.push(args);
|
|
20
|
+
}
|
|
21
|
+
close() { }
|
|
22
|
+
send() { }
|
|
23
|
+
};
|
|
24
|
+
return calls;
|
|
25
|
+
}
|
|
26
|
+
describe('ZveltioRealtime upgrade headers', () => {
|
|
27
|
+
// `/api/ws` accepts an API key in `X-API-Key`; a server-side client had no
|
|
28
|
+
// way to send one, so it could read a collection over REST and never
|
|
29
|
+
// subscribe to it.
|
|
30
|
+
it('passes headers to the socket when given', () => {
|
|
31
|
+
const calls = captureSockets();
|
|
32
|
+
const rt = new ZveltioRealtime('https://engine.test', { headers: { 'X-API-Key': 'zvk_x' } });
|
|
33
|
+
rt.connect();
|
|
34
|
+
expect(calls[0]).toEqual(['wss://engine.test/api/ws', { headers: { 'X-API-Key': 'zvk_x' } }]);
|
|
35
|
+
rt.disconnect();
|
|
36
|
+
});
|
|
37
|
+
it('uses the one-argument constructor without them', () => {
|
|
38
|
+
const calls = captureSockets();
|
|
39
|
+
const rt = new ZveltioRealtime('http://engine.test');
|
|
40
|
+
rt.connect();
|
|
41
|
+
expect(calls[0]).toEqual(['ws://engine.test/api/ws']);
|
|
42
|
+
rt.disconnect();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
/** Fake sockets that record themselves, so a test can open or close one. */
|
|
46
|
+
function fakeSockets() {
|
|
47
|
+
const made = [];
|
|
48
|
+
globalThis.WebSocket = class {
|
|
49
|
+
static CONNECTING = 0;
|
|
50
|
+
static OPEN = 1;
|
|
51
|
+
readyState = 0;
|
|
52
|
+
sent = [];
|
|
53
|
+
args;
|
|
54
|
+
onopen;
|
|
55
|
+
onclose;
|
|
56
|
+
constructor(...args) {
|
|
57
|
+
this.args = args;
|
|
58
|
+
made.push(this);
|
|
59
|
+
}
|
|
60
|
+
close() { }
|
|
61
|
+
send(p) {
|
|
62
|
+
this.sent.push(p);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return made;
|
|
66
|
+
}
|
|
67
|
+
/** Close a fake socket as the runtime does: CLOSED first, then the event. */
|
|
68
|
+
function closeWith(ws, code) {
|
|
69
|
+
ws.readyState = 3;
|
|
70
|
+
ws.onclose?.({ code });
|
|
71
|
+
}
|
|
72
|
+
/** Drive a fake socket to OPEN, as the engine accepting the upgrade would. */
|
|
73
|
+
function accept(ws) {
|
|
74
|
+
ws.readyState = 1;
|
|
75
|
+
ws.onopen?.();
|
|
76
|
+
}
|
|
77
|
+
describe('a socket the engine closed for revoked credentials', () => {
|
|
78
|
+
// The engine closes with 4001 when the session or key is revoked, and every
|
|
79
|
+
// reconnect is then refused with 401 — retrying only hammered the engine.
|
|
80
|
+
afterEach(() => jest.useRealTimers());
|
|
81
|
+
it('ZveltioRealtime stops reconnecting on 4001 and says so', () => {
|
|
82
|
+
jest.useFakeTimers();
|
|
83
|
+
const made = fakeSockets();
|
|
84
|
+
const rt = new ZveltioRealtime('http://engine.test');
|
|
85
|
+
let told = 0;
|
|
86
|
+
rt.onUnauthorized(() => told++);
|
|
87
|
+
rt.connect();
|
|
88
|
+
closeWith(made[0], 4001);
|
|
89
|
+
jest.advanceTimersByTime(60_000);
|
|
90
|
+
expect(made).toHaveLength(1);
|
|
91
|
+
expect(told).toBe(1);
|
|
92
|
+
rt.disconnect();
|
|
93
|
+
});
|
|
94
|
+
it('ZveltioRealtime still reconnects on any other close', () => {
|
|
95
|
+
jest.useFakeTimers();
|
|
96
|
+
const made = fakeSockets();
|
|
97
|
+
const rt = new ZveltioRealtime('http://engine.test');
|
|
98
|
+
let told = 0;
|
|
99
|
+
rt.onUnauthorized(() => told++);
|
|
100
|
+
rt.connect();
|
|
101
|
+
closeWith(made[0], 1006);
|
|
102
|
+
jest.advanceTimersByTime(1_000);
|
|
103
|
+
expect(made).toHaveLength(2);
|
|
104
|
+
expect(told).toBe(0);
|
|
105
|
+
rt.disconnect();
|
|
106
|
+
});
|
|
107
|
+
it('RealtimeClient stops reconnecting on 4001 and says so', () => {
|
|
108
|
+
jest.useFakeTimers();
|
|
109
|
+
const made = fakeSockets();
|
|
110
|
+
const rt = new RealtimeClient('http://engine.test');
|
|
111
|
+
let told = 0;
|
|
112
|
+
rt.onUnauthorized(() => told++);
|
|
113
|
+
rt.connect();
|
|
114
|
+
closeWith(made[0], 4001);
|
|
115
|
+
jest.advanceTimersByTime(60_000);
|
|
116
|
+
expect(made).toHaveLength(1);
|
|
117
|
+
expect(told).toBe(1);
|
|
118
|
+
rt.disconnect();
|
|
119
|
+
});
|
|
120
|
+
it('RealtimeClient still reconnects on any other close', () => {
|
|
121
|
+
jest.useFakeTimers();
|
|
122
|
+
const made = fakeSockets();
|
|
123
|
+
const rt = new RealtimeClient('http://engine.test');
|
|
124
|
+
rt.connect();
|
|
125
|
+
closeWith(made[0], 1006);
|
|
126
|
+
jest.advanceTimersByTime(1_000);
|
|
127
|
+
expect(made).toHaveLength(2);
|
|
128
|
+
rt.disconnect();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('RealtimeClient after a revocation and while the engine refuses it', () => {
|
|
132
|
+
afterEach(() => jest.useRealTimers());
|
|
133
|
+
it('after 4001, subscribe() only records; an explicit connect() sends it', () => {
|
|
134
|
+
jest.useFakeTimers();
|
|
135
|
+
const made = fakeSockets();
|
|
136
|
+
const rt = new RealtimeClient('http://engine.test');
|
|
137
|
+
rt.connect();
|
|
138
|
+
closeWith(made[0], 4001);
|
|
139
|
+
// It used to dial again here, be refused with 401 (seen as 1006) and back
|
|
140
|
+
// off forever.
|
|
141
|
+
rt.subscribe('orders', '*', () => { });
|
|
142
|
+
jest.advanceTimersByTime(600_000);
|
|
143
|
+
expect(made).toHaveLength(1);
|
|
144
|
+
rt.connect(); // the app re-authenticated
|
|
145
|
+
expect(made).toHaveLength(2);
|
|
146
|
+
accept(made[1]);
|
|
147
|
+
expect(made[1].sent).toEqual([JSON.stringify({ type: 'subscribe', channel: 'orders:*' })]);
|
|
148
|
+
rt.disconnect();
|
|
149
|
+
});
|
|
150
|
+
it('stops after a bounded number of reconnects', () => {
|
|
151
|
+
jest.useFakeTimers();
|
|
152
|
+
const made = fakeSockets();
|
|
153
|
+
const rt = new RealtimeClient('http://engine.test');
|
|
154
|
+
rt.connect();
|
|
155
|
+
// A refused handshake: no runtime shows the 401, only a 1006/1002 close.
|
|
156
|
+
for (let i = 0; i < 20; i++) {
|
|
157
|
+
closeWith(made[made.length - 1], 1006);
|
|
158
|
+
jest.advanceTimersByTime(30_000);
|
|
159
|
+
}
|
|
160
|
+
expect(made).toHaveLength(11); // the first dial and ten retries
|
|
161
|
+
rt.disconnect();
|
|
162
|
+
});
|
|
163
|
+
it('sends its headers with the upgrade', () => {
|
|
164
|
+
const made = fakeSockets();
|
|
165
|
+
const rt = new RealtimeClient('https://engine.test', { headers: { 'X-API-Key': 'zvk_x' } });
|
|
166
|
+
rt.connect();
|
|
167
|
+
expect(made[0].args).toEqual([
|
|
168
|
+
'wss://engine.test/api/ws',
|
|
169
|
+
{ headers: { 'X-API-Key': 'zvk_x' } },
|
|
170
|
+
]);
|
|
171
|
+
rt.disconnect();
|
|
172
|
+
});
|
|
173
|
+
it('ZveltioClient sends its API key on the socket and reports a 4001 as unauthorized', () => {
|
|
174
|
+
const made = fakeSockets();
|
|
175
|
+
let told = 0;
|
|
176
|
+
const client = new ZveltioClient({
|
|
177
|
+
baseUrl: 'https://engine.test',
|
|
178
|
+
apiKey: 'zvk_y',
|
|
179
|
+
onUnauthorized: () => told++,
|
|
180
|
+
});
|
|
181
|
+
client.realtime.connect();
|
|
182
|
+
expect(made[0].args).toEqual([
|
|
183
|
+
'wss://engine.test/api/ws',
|
|
184
|
+
{ headers: { 'X-API-Key': 'zvk_y' } },
|
|
185
|
+
]);
|
|
186
|
+
closeWith(made[0], 4001);
|
|
187
|
+
expect(told).toBe(1);
|
|
188
|
+
client.realtime.disconnect();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
describe('watchSchema', () => {
|
|
192
|
+
// It dialled the socket with no credentials, so the engine refused every
|
|
193
|
+
// upgrade (401) and the watcher reconnected every few seconds, forever.
|
|
194
|
+
it('sends its API key with the upgrade', async () => {
|
|
195
|
+
let upgradeKey;
|
|
196
|
+
const server = Bun.serve({
|
|
197
|
+
port: 0,
|
|
198
|
+
fetch(req, srv) {
|
|
199
|
+
const url = new URL(req.url);
|
|
200
|
+
if (url.pathname === '/api/collections')
|
|
201
|
+
return Response.json({ collections: [] });
|
|
202
|
+
if (url.pathname === '/api/ws') {
|
|
203
|
+
upgradeKey = req.headers.get('x-api-key');
|
|
204
|
+
if (upgradeKey !== 'zvk_watch')
|
|
205
|
+
return new Response('Unauthorized', { status: 401 });
|
|
206
|
+
return srv.upgrade(req) ? undefined : new Response('no upgrade', { status: 426 });
|
|
207
|
+
}
|
|
208
|
+
return new Response('not found', { status: 404 });
|
|
209
|
+
},
|
|
210
|
+
websocket: { message() { } },
|
|
211
|
+
});
|
|
212
|
+
const out = `${tmpdir()}/zveltio-watch-${Date.now()}.d.ts`;
|
|
213
|
+
const log = spyOn(console, 'log').mockImplementation(() => { });
|
|
214
|
+
const stop = await watchSchema(`http://127.0.0.1:${server.port}`, out, { apiKey: 'zvk_watch' });
|
|
215
|
+
try {
|
|
216
|
+
for (let i = 0; i < 100 && upgradeKey === undefined; i++)
|
|
217
|
+
await Bun.sleep(10);
|
|
218
|
+
expect(upgradeKey).toBe('zvk_watch');
|
|
219
|
+
for (let i = 0; i < 100 && !log.mock.calls.some((c) => String(c[0]).includes('connected')); i++) {
|
|
220
|
+
await Bun.sleep(10);
|
|
221
|
+
}
|
|
222
|
+
expect(log.mock.calls.some((c) => String(c[0]).includes('Schema watcher connected'))).toBe(true);
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
stop();
|
|
226
|
+
log.mockRestore();
|
|
227
|
+
server.stop(true);
|
|
228
|
+
await rm(out, { force: true });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
// The engine publishes schema changes on `$schema` as `schema:changed`, to a
|
|
232
|
+
// schema admin's session only — so the watcher must be able to send one.
|
|
233
|
+
it('regenerates on a schema:changed event, and reports a refused subscribe', async () => {
|
|
234
|
+
let upgradeCookie = null;
|
|
235
|
+
const subscribed = [];
|
|
236
|
+
const server = Bun.serve({
|
|
237
|
+
port: 0,
|
|
238
|
+
fetch(req, srv) {
|
|
239
|
+
const url = new URL(req.url);
|
|
240
|
+
if (url.pathname === '/api/collections')
|
|
241
|
+
return Response.json({ collections: [] });
|
|
242
|
+
if (url.pathname === '/api/ws') {
|
|
243
|
+
upgradeCookie = req.headers.get('cookie');
|
|
244
|
+
return srv.upgrade(req) ? undefined : new Response('no upgrade', { status: 426 });
|
|
245
|
+
}
|
|
246
|
+
return new Response('not found', { status: 404 });
|
|
247
|
+
},
|
|
248
|
+
websocket: {
|
|
249
|
+
message(ws, raw) {
|
|
250
|
+
const msg = JSON.parse(String(raw));
|
|
251
|
+
subscribed.push(msg);
|
|
252
|
+
if (msg.channel !== '$schema')
|
|
253
|
+
return;
|
|
254
|
+
ws.send(JSON.stringify({ type: 'subscribed', channel: '$schema' }));
|
|
255
|
+
ws.send(JSON.stringify({ type: 'schema:changed', collection: 'orders', action: 'alter' }));
|
|
256
|
+
ws.send(JSON.stringify({ type: 'error', message: 'No read permission for "$schema"' }));
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
const out = `${tmpdir()}/zveltio-watch-evt-${Date.now()}.d.ts`;
|
|
261
|
+
const log = spyOn(console, 'log').mockImplementation(() => { });
|
|
262
|
+
let updates = 0;
|
|
263
|
+
const errors = [];
|
|
264
|
+
const stop = await watchSchema(`http://127.0.0.1:${server.port}`, out, {
|
|
265
|
+
headers: { cookie: 'session=abc' },
|
|
266
|
+
onUpdate: () => updates++,
|
|
267
|
+
onError: (e) => errors.push(e.message),
|
|
268
|
+
});
|
|
269
|
+
try {
|
|
270
|
+
for (let i = 0; i < 200 && (updates < 2 || errors.length === 0); i++)
|
|
271
|
+
await Bun.sleep(10);
|
|
272
|
+
expect(upgradeCookie).toBe('session=abc');
|
|
273
|
+
expect(subscribed).toEqual([{ type: 'subscribe', channel: '$schema' }]);
|
|
274
|
+
expect(updates).toBe(2); // the initial generation, then the event
|
|
275
|
+
expect(errors).toEqual(['Schema watcher: No read permission for "$schema"']);
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
stop();
|
|
279
|
+
log.mockRestore();
|
|
280
|
+
server.stop(true);
|
|
281
|
+
await rm(out, { force: true });
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-manager.test.d.ts","sourceRoot":"","sources":["../../src/tests/sync-manager.test.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import './setup';
|
|
2
|
+
import { describe, it, expect, afterEach } from 'bun:test';
|
|
3
|
+
import { SyncManager } from '../sync-manager.js';
|
|
4
|
+
/**
|
|
5
|
+
* A client whose `get` answers the server's copy of a record — the only call the
|
|
6
|
+
* realtime handler makes.
|
|
7
|
+
*/
|
|
8
|
+
function fakeClient() {
|
|
9
|
+
return {
|
|
10
|
+
collection: () => ({ get: async (id) => ({ id, title: 'from server' }) }),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** Stands in for `ZveltioRealtime`; `push` delivers a frame as the socket would. */
|
|
14
|
+
function fakeRealtime() {
|
|
15
|
+
let handler;
|
|
16
|
+
return {
|
|
17
|
+
subscribe: (_collection, cb) => {
|
|
18
|
+
handler = cb;
|
|
19
|
+
return () => { };
|
|
20
|
+
},
|
|
21
|
+
disconnect: () => { },
|
|
22
|
+
push: async (msg) => handler?.(msg),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
let sync;
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
await sync?.stop();
|
|
28
|
+
sync = undefined;
|
|
29
|
+
});
|
|
30
|
+
describe('SyncManager — realtime frames as the engine sends them', () => {
|
|
31
|
+
// The frame shape is `broadcastEvent` in packages/engine/src/routes/ws.ts.
|
|
32
|
+
// The handler used to wait for `record.created` / `record_id`, names the
|
|
33
|
+
// socket never sends, so no server push ever reached the local store.
|
|
34
|
+
it('applies an insert and drops a delete without queueing one back', async () => {
|
|
35
|
+
sync = new SyncManager(fakeClient(), { syncInterval: 60_000 });
|
|
36
|
+
await sync.start();
|
|
37
|
+
const store = sync.store;
|
|
38
|
+
await store.clear();
|
|
39
|
+
const rt = fakeRealtime();
|
|
40
|
+
sync.realtime = rt;
|
|
41
|
+
let latest = [];
|
|
42
|
+
sync.collection('posts').subscribe((records) => {
|
|
43
|
+
latest = records;
|
|
44
|
+
});
|
|
45
|
+
await rt.push({ type: 'event', collection: 'posts', event: 'insert', data: { id: 'p1' } });
|
|
46
|
+
expect((await store.get('posts', 'p1'))?.data).toMatchObject({ title: 'from server' });
|
|
47
|
+
expect(latest.map((r) => r.id)).toEqual(['p1']);
|
|
48
|
+
await rt.push({ type: 'event', collection: 'posts', event: 'delete', data: { id: 'p1' } });
|
|
49
|
+
expect(await store.get('posts', 'p1')).toBeUndefined();
|
|
50
|
+
// A queued delete here would send the server's own deletion back to it.
|
|
51
|
+
expect(await store.getPendingOps()).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('SyncManager — stop() before start() settles', () => {
|
|
55
|
+
// A React effect cleanup (and StrictMode, on every mount in development) calls
|
|
56
|
+
// `stop()` while `start()` is still awaiting the store. `start()` then armed
|
|
57
|
+
// the sync interval anyway, and nothing was left to clear it.
|
|
58
|
+
it('leaves no timer armed', async () => {
|
|
59
|
+
sync = new SyncManager(fakeClient(), { syncInterval: 60_000 });
|
|
60
|
+
const starting = sync.start();
|
|
61
|
+
await sync.stop();
|
|
62
|
+
await starting;
|
|
63
|
+
expect(sync.syncTimer).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-test-db.test.d.ts","sourceRoot":"","sources":["../../src/tests/with-test-db.test.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import './setup';
|
|
2
|
+
import { describe, it, expect } from 'bun:test';
|
|
3
|
+
import { Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler, } from 'kysely';
|
|
4
|
+
import { applyMigrationStrings } from '../testing/with-test-db.js';
|
|
5
|
+
/**
|
|
6
|
+
* The harness other suites trust.
|
|
7
|
+
*
|
|
8
|
+
* A Zveltio migration keeps its rollback in the same file behind a `-- DOWN`
|
|
9
|
+
* marker, and the documented way to use this helper is to glob
|
|
10
|
+
* `engine/migrations/*.sql` and hand every file to it. Running the whole file
|
|
11
|
+
* creates the schema and then drops it again inside the same call.
|
|
12
|
+
*/
|
|
13
|
+
function recordingDb(sink) {
|
|
14
|
+
const connection = {
|
|
15
|
+
async executeQuery(compiled) {
|
|
16
|
+
sink.push(compiled.sql);
|
|
17
|
+
return { rows: [] };
|
|
18
|
+
},
|
|
19
|
+
async *streamQuery() { },
|
|
20
|
+
};
|
|
21
|
+
const driver = {
|
|
22
|
+
async init() { },
|
|
23
|
+
async acquireConnection() {
|
|
24
|
+
return connection;
|
|
25
|
+
},
|
|
26
|
+
async beginTransaction() { },
|
|
27
|
+
async commitTransaction() { },
|
|
28
|
+
async rollbackTransaction() { },
|
|
29
|
+
async releaseConnection() { },
|
|
30
|
+
async destroy() { },
|
|
31
|
+
};
|
|
32
|
+
return new Kysely({
|
|
33
|
+
dialect: {
|
|
34
|
+
createAdapter: () => new PostgresAdapter(),
|
|
35
|
+
createDriver: () => driver,
|
|
36
|
+
createIntrospector: (db) => new PostgresIntrospector(db),
|
|
37
|
+
createQueryCompiler: () => new PostgresQueryCompiler(),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
describe('applyMigrationStrings', () => {
|
|
42
|
+
it('does not run the DOWN half of a migration', async () => {
|
|
43
|
+
const executed = [];
|
|
44
|
+
const db = recordingDb(executed);
|
|
45
|
+
await applyMigrationStrings(db, ['CREATE TABLE zvd_t (id TEXT);\n-- DOWN\nDROP TABLE zvd_t;']);
|
|
46
|
+
expect(executed.join('\n')).toContain('CREATE TABLE');
|
|
47
|
+
expect(executed.join('\n'), 'the rollback ran against the schema it had just created').not.toContain('DROP TABLE');
|
|
48
|
+
await db.destroy();
|
|
49
|
+
});
|
|
50
|
+
it('still runs every statement of a file with no DOWN section', async () => {
|
|
51
|
+
const executed = [];
|
|
52
|
+
const db = recordingDb(executed);
|
|
53
|
+
await applyMigrationStrings(db, ['CREATE TABLE a (id TEXT);\nCREATE TABLE b (id TEXT);']);
|
|
54
|
+
expect(executed).toHaveLength(2);
|
|
55
|
+
await db.destroy();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAEvB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAGD,MAAM,WAAW,aAAa,CAAC,CAAC,GAAG,GAAG;IACpC,OAAO,EAAE,CAAC,EAAE,CAAC;IACb,UAAU,EAAE;QACV,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAGD,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,GAAG;IACrC,MAAM,EAAE,CAAC,CAAC;CACX;AAGD,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,GAAG;IACrC,MAAM,EAAE,CAAC,CAAC;CACX;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC;IAEnB,IAAI,EAAE,GAAG,CAAC;IACV,SAAS,EAAE,MAAM,CAAC;CACnB"}
|
package/dist/validate/index.d.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* - Building the Studio bundle. Too slow; pair with `zveltio extension build`
|
|
24
24
|
* which can call validate as a sub-step.
|
|
25
25
|
*/
|
|
26
|
+
export { SHARED_MESSAGE_KEYS } from './shared-message-keys.js';
|
|
26
27
|
export interface ValidationError {
|
|
27
28
|
/** Stable machine-readable code (e.g. `MANIFEST_NAME_MISMATCH`). */
|
|
28
29
|
code: string;
|
|
@@ -30,6 +31,8 @@ export interface ValidationError {
|
|
|
30
31
|
message: string;
|
|
31
32
|
/** Optional file path the error applies to, relative to the extension root. */
|
|
32
33
|
file?: string;
|
|
34
|
+
/** `warning` items are surfaced but do NOT fail validation (`result.ok`). */
|
|
35
|
+
severity?: 'error' | 'warning';
|
|
33
36
|
}
|
|
34
37
|
export interface ValidationResult {
|
|
35
38
|
ok: boolean;
|
|
@@ -79,6 +82,52 @@ export interface BundleSizeInput {
|
|
|
79
82
|
bundleSizeKbMax?: number;
|
|
80
83
|
}
|
|
81
84
|
export declare function validateBundleSize(input: BundleSizeInput): ValidationError[];
|
|
85
|
+
/** Read-only core endpoints a schema is legitimately allowed to call (relation
|
|
86
|
+
* pickers etc.). Keep this list tiny and read-only. */
|
|
87
|
+
export declare const SDUI_SHARED_READ_ALLOWLIST: ReadonlySet<string>;
|
|
88
|
+
/**
|
|
89
|
+
* Schema keys whose value is user-visible text.
|
|
90
|
+
*
|
|
91
|
+
* The host renders these through `t()`, which looks the string up in the
|
|
92
|
+
* message bundle and falls back to the literal. That fallback is why a typo is
|
|
93
|
+
* invisible at runtime: the user simply sees `auth.saml.ui.begin_certificate`
|
|
94
|
+
* rendered as a placeholder. Catching it here is the only place it is cheap.
|
|
95
|
+
*/
|
|
96
|
+
export declare const SDUI_I18N_SLOTS: readonly string[];
|
|
97
|
+
export interface SduiValidationInput {
|
|
98
|
+
/** Parsed schema JSON (a PageSchema or SettingsSchema). */
|
|
99
|
+
schema: unknown;
|
|
100
|
+
/** Owning extension name, e.g. `finance/invoicing`. */
|
|
101
|
+
extName: string;
|
|
102
|
+
/** Route paths the extension's engine actually serves, relative to its mount
|
|
103
|
+
* (e.g. `['/invoices', '/invoices/:id/pay']`). The CLI extracts these from
|
|
104
|
+
* `engine/**` source; pass `undefined` to skip the path-existence check
|
|
105
|
+
* (foreign-namespace check still runs). */
|
|
106
|
+
providedRoutes?: string[];
|
|
107
|
+
/** Extension names this one declares as dependencies (manifest.dependencies).
|
|
108
|
+
* A schema may read a dependency's `/ext/<dep>/` routes (relation pickers),
|
|
109
|
+
* which is otherwise treated as foreign. */
|
|
110
|
+
dependencies?: string[];
|
|
111
|
+
/** Message keys this schema may resolve against: the extension's own
|
|
112
|
+
* `studio/messages/en.json` keys, unioned with `SHARED_MESSAGE_KEYS`.
|
|
113
|
+
* Omit to skip the i18n checks (endpoint checks still run). */
|
|
114
|
+
messageKeys?: Iterable<string>;
|
|
115
|
+
/** For diagnostics. */
|
|
116
|
+
file?: string;
|
|
117
|
+
}
|
|
118
|
+
export declare function validateSduiSchema(input: SduiValidationInput): ValidationError[];
|
|
119
|
+
/** Extract route paths an extension serves from its engine source text.
|
|
120
|
+
*
|
|
121
|
+
* Heuristic over `.get/post/put/patch/delete('…')` and `.route('/prefix', …)`
|
|
122
|
+
* mounts. Nested Hono sub-apps (e.g. `app.route('/query', aiQueryRoutes)`) used
|
|
123
|
+
* to be invisible here, so schemas pointing at the real `/query/history` path
|
|
124
|
+
* failed `SDUI_ENDPOINT_UNKNOWN` while a wrong bare `/history` falsely passed.
|
|
125
|
+
*
|
|
126
|
+
* Mounts register `/prefix` and `/prefix/*`; matching allows any path under that
|
|
127
|
+
* prefix (see `isProvidedRoute`). Parsing each factory body is intentionally
|
|
128
|
+
* avoided — route handlers often contain regexes/`${…}` that defeat brace
|
|
129
|
+
* matching. */
|
|
130
|
+
export declare function extractEngineRoutes(engineSourceTexts: string[]): string[];
|
|
82
131
|
export interface ValidateExtensionInput {
|
|
83
132
|
manifest: ManifestValidationInput;
|
|
84
133
|
peerDeps: PeerDepsValidationInput;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/validate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,MAAM,WAAW,eAAe;IAC9B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/validate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE/D,MAAM,WAAW,eAAe;IAC9B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,oDAAoD;IACpD,KAAK,EAAE;QACL,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAmDD,MAAM,WAAW,uBAAuB;IACtC,+DAA+D;IAC/D,QAAQ,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,uBAAuB,GAAG,eAAe,EAAE,CA2ElF;AAID,MAAM,WAAW,uBAAuB;IACtC,wEAAwE;IACxE,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IACrD,iFAAiF;IACjF,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACtC;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,uBAAuB,GAAG,eAAe,EAAE,CAyC1F;AAID,MAAM,WAAW,yBAAyB;IACxC,2EAA2E;IAC3E,KAAK,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChD,qFAAqF;IACrF,yBAAyB,CAAC,EAAE,OAAO,CAAC;CACrC;AA+KD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,yBAAyB,GAAG,eAAe,EAAE,CA+FtF;AAID,MAAM,WAAW,iBAAiB;IAChC,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,iBAAiB,GAAG,eAAe,EAAE,CAQhF;AAID,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;IACpB,wCAAwC;IACxC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAID,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,eAAe,EAAE,CAY5E;AAWD;uDACuD;AACvD,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,MAAM,CAGzD,CAAC;AAcH;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,MAAM,EAW5C,CAAC;AAwHF,MAAM,WAAW,mBAAmB;IAClC,2DAA2D;IAC3D,MAAM,EAAE,OAAO,CAAC;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB;;;+CAG2C;IAC3C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;gDAE4C;IAC5C,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB;;mEAE+D;IAC/D,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC/B,uBAAuB;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,mBAAmB,GAAG,eAAe,EAAE,CAuFhF;AAsBD;;;;;;;;;;eAUe;AACf,wBAAgB,mBAAmB,CAAC,iBAAiB,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAoBzE;AAID,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,uBAAuB,CAAC;IAClC,QAAQ,EAAE,uBAAuB,CAAC;IAClC,UAAU,EAAE,yBAAyB,CAAC;IACtC,YAAY,EAAE,iBAAiB,CAAC;IAChC,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,qEAAqE;IACrE,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,gBAAgB,CAkBjF"}
|