@zveltio/sdk 3.0.0-beta.77 → 3.0.0-beta.79
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/ddl/index.d.ts +3 -0
- package/dist/ddl/index.d.ts.map +1 -1
- package/dist/extension/capabilities.d.ts +70 -0
- package/dist/extension/capabilities.d.ts.map +1 -0
- package/dist/extension/capabilities.js +78 -0
- package/dist/extension/index.d.ts +104 -10
- package/dist/extension/index.d.ts.map +1 -1
- package/dist/extension/index.js +4 -0
- package/dist/offline/index.d.ts +20 -51
- package/dist/offline/index.d.ts.map +1 -1
- package/dist/offline/index.js +114 -140
- package/dist/schema-codegen.d.ts +22 -0
- package/dist/schema-codegen.d.ts.map +1 -1
- package/dist/schema-codegen.js +100 -0
- package/dist/testing/with-test-db.d.ts +1 -1
- package/dist/testing/with-test-db.d.ts.map +1 -1
- package/dist/testing/with-test-db.js +1 -1
- package/dist/tests/migration-parse.test.d.ts +2 -0
- package/dist/tests/migration-parse.test.d.ts.map +1 -0
- package/dist/tests/migration-parse.test.js +17 -0
- package/dist/tests/validate-manifest-dependencies.test.d.ts +2 -0
- package/dist/tests/validate-manifest-dependencies.test.d.ts.map +1 -0
- package/dist/tests/validate-manifest-dependencies.test.js +27 -0
- package/dist/tests/validate-manifest-forward-credentials.test.d.ts +2 -0
- package/dist/tests/validate-manifest-forward-credentials.test.d.ts.map +1 -0
- package/dist/tests/validate-manifest-forward-credentials.test.js +22 -0
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +51 -0
- package/dist/validate/migration-parse.d.ts +10 -5
- package/dist/validate/migration-parse.d.ts.map +1 -1
- package/dist/validate/migration-parse.js +1 -1
- package/package.json +2 -2
package/dist/offline/index.js
CHANGED
|
@@ -9,51 +9,30 @@
|
|
|
9
9
|
* offline, applies on reconnect, handles conflicts. No external
|
|
10
10
|
* dependency beyond the SDK + engine.
|
|
11
11
|
*
|
|
12
|
-
* - **`electric`** — Electric
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* - **`electric`** — Electric 1.x shapes, served THROUGH the engine:
|
|
13
|
+
* `GET {engineUrl}/api/electric/v1/shape?collection=…`. The engine decides
|
|
14
|
+
* the table, rows and columns from the caller's grants, tenant, row rules
|
|
15
|
+
* and column permissions, and proxies Electric's Shape protocol (initial
|
|
16
|
+
* snapshot, then long-poll `live=true`). Read-only: writes go through the
|
|
17
|
+
* data API, and Electric streams them back. Requires the engine's
|
|
18
|
+
* ELECTRIC_URL + ELECTRIC_SECRET; the client never talks to Electric.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* credentials; the engine reads the better-auth session).
|
|
23
|
-
* 2. Engine returns `{ token, expiresAt, electricUrl }`.
|
|
24
|
-
* 3. Provider opens a websocket to `${electricUrl}?token=<jwt>`.
|
|
25
|
-
* 4. Before each token expires, the provider re-requests a fresh one
|
|
26
|
-
* (no user interaction).
|
|
27
|
-
*
|
|
28
|
-
* Pull/push semantics
|
|
29
|
-
* -------------------
|
|
30
|
-
* Electric replicates continuously over the websocket — there's no
|
|
31
|
-
* explicit "pull" call. The provider's `pull()` is a no-op that returns
|
|
32
|
-
* once the initial sync handshake completes. `push()` is the same: local
|
|
33
|
-
* writes go through Electric's normal replication path automatically.
|
|
34
|
-
* Both shims exist so the OfflineProvider interface stays uniform across
|
|
35
|
-
* crdt/electric.
|
|
36
|
-
*
|
|
37
|
-
* Subscriptions
|
|
38
|
-
* -------------
|
|
39
|
-
* `subscribe('zvd_contacts', cb)` registers a callback fired whenever
|
|
40
|
-
* Electric pushes a change for that table. The implementation uses the
|
|
41
|
-
* websocket's `message` event with a routing table; one socket, many
|
|
42
|
-
* subscriptions.
|
|
20
|
+
* `subscribe('contacts', cb)` keeps one live shape per collection and calls
|
|
21
|
+
* `cb` with every current row after each change. `pull()` brings each of
|
|
22
|
+
* `tables` up to date once.
|
|
43
23
|
*/
|
|
44
24
|
export class ElectricNotConfigured extends Error {
|
|
45
25
|
constructor(reason) {
|
|
46
26
|
super(`Electric SQL provider is not configured: ${reason}. ` +
|
|
47
|
-
`Set provider: 'crdt' for the default sync path
|
|
48
|
-
`service and pass electricUrl. See docs/engine/offline-sync.md.`);
|
|
27
|
+
`Set provider: 'crdt' for the default sync path. See docs/engine/offline-sync.md.`);
|
|
49
28
|
this.name = 'ElectricNotConfigured';
|
|
50
29
|
}
|
|
51
30
|
}
|
|
52
31
|
export class ElectricUnavailable extends Error {
|
|
53
32
|
constructor(reason) {
|
|
54
33
|
super(`Electric SQL is unavailable: ${reason}. ` +
|
|
55
|
-
`The engine
|
|
56
|
-
`Electric service is down. Fall back to provider: 'crdt' or check the ops checklist.`);
|
|
34
|
+
`The engine refused the shape, reports ELECTRIC_URL / ELECTRIC_SECRET unset, or ` +
|
|
35
|
+
`the Electric service is down. Fall back to provider: 'crdt' or check the ops checklist.`);
|
|
57
36
|
this.name = 'ElectricUnavailable';
|
|
58
37
|
}
|
|
59
38
|
}
|
|
@@ -62,11 +41,9 @@ export class ElectricUnavailable extends Error {
|
|
|
62
41
|
*
|
|
63
42
|
* For `crdt` (default): returns a thin adapter around `SyncManager`.
|
|
64
43
|
*
|
|
65
|
-
* For `electric`:
|
|
66
|
-
* `ElectricNotConfigured`
|
|
67
|
-
*
|
|
68
|
-
* lazily on the first network op when the engine reports Electric is
|
|
69
|
-
* disabled.
|
|
44
|
+
* For `electric`: syncs `tables` once before resolving. Throws
|
|
45
|
+
* `ElectricNotConfigured` when there is no `fetch`, and `ElectricUnavailable`
|
|
46
|
+
* when the engine refuses a shape (401, 403, 409 with its reason, 503).
|
|
70
47
|
*/
|
|
71
48
|
export async function createOfflineProvider(config) {
|
|
72
49
|
const kind = config.provider ?? 'crdt';
|
|
@@ -141,131 +118,128 @@ async function makeElectricProvider(config) {
|
|
|
141
118
|
if (typeof doFetch !== 'function') {
|
|
142
119
|
throw new ElectricNotConfigured('no fetch implementation available — pass config.fetch');
|
|
143
120
|
}
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
121
|
+
const shapes = new Map();
|
|
122
|
+
const subscribers = new Map();
|
|
123
|
+
const loops = new Map();
|
|
124
|
+
// The engine names collections; `zvd_contacts` is accepted for the old table spelling.
|
|
125
|
+
const nameOf = (table) => table.replace(/^zvd_/, '');
|
|
126
|
+
const notify = (name) => {
|
|
127
|
+
const rows = [...(shapes.get(name)?.rows.values() ?? [])];
|
|
128
|
+
for (const cb of subscribers.get(name) ?? [])
|
|
129
|
+
cb(rows);
|
|
130
|
+
};
|
|
131
|
+
/** One Shape-protocol request. Resolves true once the shape is up to date. */
|
|
132
|
+
async function step(name, live, signal) {
|
|
133
|
+
const st = shapes.get(name) ?? { rows: new Map(), offset: '-1' };
|
|
134
|
+
shapes.set(name, st);
|
|
135
|
+
const qs = new URLSearchParams({ collection: name, offset: st.offset });
|
|
136
|
+
if (st.handle)
|
|
137
|
+
qs.set('handle', st.handle);
|
|
138
|
+
if (live)
|
|
139
|
+
qs.set('live', 'true');
|
|
140
|
+
if (live && st.cursor)
|
|
141
|
+
qs.set('cursor', st.cursor);
|
|
142
|
+
const res = await doFetch(`${config.engineUrl}/api/electric/v1/shape?${qs}`, {
|
|
153
143
|
credentials: 'include',
|
|
154
|
-
headers:
|
|
155
|
-
|
|
144
|
+
headers: config.headers,
|
|
145
|
+
signal,
|
|
156
146
|
});
|
|
157
|
-
if (res.
|
|
158
|
-
throw new ElectricUnavailable('engine returned 401 — sign in before requesting Electric sync');
|
|
159
|
-
}
|
|
160
|
-
if (res.status === 503) {
|
|
147
|
+
if (!res.ok) {
|
|
161
148
|
const body = (await res.json().catch(() => ({})));
|
|
162
|
-
|
|
149
|
+
if (res.status === 409 && body.code === 'electric.must_refetch') {
|
|
150
|
+
// The shape was rebuilt (a rule or grant changed): start over.
|
|
151
|
+
shapes.set(name, { rows: new Map(), offset: '-1' });
|
|
152
|
+
notify(name);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
throw new ElectricUnavailable(`engine returned ${res.status}${body.code ? ` ${body.code}` : ''}: ` +
|
|
156
|
+
(body.detail ?? body.error ?? 'no detail'));
|
|
163
157
|
}
|
|
164
|
-
|
|
165
|
-
|
|
158
|
+
const messages = (await res.json());
|
|
159
|
+
st.offset = res.headers.get('electric-offset') ?? st.offset;
|
|
160
|
+
st.handle = res.headers.get('electric-handle') ?? st.handle;
|
|
161
|
+
st.cursor = res.headers.get('electric-cursor') ?? st.cursor;
|
|
162
|
+
let changed = false;
|
|
163
|
+
let upToDate = false;
|
|
164
|
+
for (const m of messages) {
|
|
165
|
+
if (m.headers.control === 'up-to-date')
|
|
166
|
+
upToDate = true;
|
|
167
|
+
if (!m.key)
|
|
168
|
+
continue;
|
|
169
|
+
changed = true;
|
|
170
|
+
if (m.headers.operation === 'delete')
|
|
171
|
+
st.rows.delete(m.key);
|
|
172
|
+
// An update carries only the changed columns.
|
|
173
|
+
else
|
|
174
|
+
st.rows.set(m.key, { ...st.rows.get(m.key), ...m.value });
|
|
166
175
|
}
|
|
167
|
-
|
|
176
|
+
if (changed)
|
|
177
|
+
notify(name);
|
|
178
|
+
return upToDate;
|
|
168
179
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
180
|
+
async function syncOnce(name) {
|
|
181
|
+
for (let i = 0; i < 1000 && !(await step(name, false)); i++)
|
|
182
|
+
;
|
|
183
|
+
}
|
|
184
|
+
function follow(name) {
|
|
185
|
+
if (loops.has(name))
|
|
186
|
+
return;
|
|
187
|
+
const ctl = new AbortController();
|
|
188
|
+
loops.set(name, ctl);
|
|
189
|
+
void (async () => {
|
|
190
|
+
let backoff = 1_000;
|
|
191
|
+
let upToDate = false;
|
|
192
|
+
while (!ctl.signal.aborted) {
|
|
193
|
+
try {
|
|
194
|
+
upToDate = await step(name, upToDate, ctl.signal);
|
|
195
|
+
backoff = 1_000;
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
if (ctl.signal.aborted)
|
|
199
|
+
return;
|
|
200
|
+
// A refusal (403, 409) stays a refusal until something changes; retry slowly.
|
|
201
|
+
console.warn(`[offline:electric] ${name}:`, err.message);
|
|
202
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
203
|
+
backoff = Math.min(backoff * 2, 30_000);
|
|
190
204
|
}
|
|
191
205
|
}
|
|
192
|
-
|
|
193
|
-
console.warn('[offline:electric] token refresh failed:', err.message);
|
|
194
|
-
}
|
|
195
|
-
}, delay);
|
|
206
|
+
})();
|
|
196
207
|
}
|
|
197
|
-
|
|
198
|
-
// `${baseUrl}?token=<jwt>`; we forward the URL the engine gave us.
|
|
199
|
-
let socket;
|
|
200
|
-
const handshake = new Promise((resolve, reject) => {
|
|
201
|
-
const url = `${electricUrl}${electricUrl.includes('?') ? '&' : '?'}token=${encodeURIComponent(auth.token)}`;
|
|
202
|
-
socket = new WS(url);
|
|
203
|
-
socket.addEventListener('open', () => {
|
|
204
|
-
resolve();
|
|
205
|
-
scheduleRefresh();
|
|
206
|
-
});
|
|
207
|
-
socket.addEventListener('error', (ev) => {
|
|
208
|
-
// The first error fires before `open`; treat it as a connect failure.
|
|
209
|
-
reject(new ElectricUnavailable(`websocket error: ${ev.message ?? 'unknown'}`));
|
|
210
|
-
});
|
|
211
|
-
socket.addEventListener('message', (ev) => {
|
|
212
|
-
let msg;
|
|
213
|
-
try {
|
|
214
|
-
msg = JSON.parse(ev.data);
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
217
|
-
return; // Electric sometimes sends binary heartbeats — skip parse errors.
|
|
218
|
-
}
|
|
219
|
-
if (msg.type === 'change' && msg.table && Array.isArray(msg.rows)) {
|
|
220
|
-
const subs = subscribers.get(msg.table);
|
|
221
|
-
if (subs)
|
|
222
|
-
for (const cb of subs)
|
|
223
|
-
cb(msg.rows);
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
await handshake;
|
|
228
|
-
return {
|
|
208
|
+
const provider = {
|
|
229
209
|
kind: 'electric',
|
|
230
210
|
async pull() {
|
|
231
|
-
//
|
|
232
|
-
|
|
211
|
+
// A followed collection is already kept current by its live loop.
|
|
212
|
+
for (const t of config.tables ?? [])
|
|
213
|
+
if (!loops.has(nameOf(t)))
|
|
214
|
+
await syncOnce(nameOf(t));
|
|
233
215
|
},
|
|
234
216
|
async push() {
|
|
235
|
-
|
|
236
|
-
|
|
217
|
+
throw new Error('The electric provider syncs reads only: write through the data API ' +
|
|
218
|
+
'(client.collection(name).create/update/delete); Electric streams the change back.');
|
|
237
219
|
},
|
|
238
220
|
subscribe(table, cb) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
subscribers.set(table, set);
|
|
243
|
-
// Tell Electric we're interested in this table — the protocol
|
|
244
|
-
// is documented in Electric's docs as `{ type: 'subscribe', table }`.
|
|
245
|
-
if (socket.readyState === WS.OPEN) {
|
|
246
|
-
socket.send(JSON.stringify({ type: 'subscribe', table }));
|
|
247
|
-
}
|
|
248
|
-
}
|
|
221
|
+
const name = nameOf(table);
|
|
222
|
+
const set = subscribers.get(name) ?? new Set();
|
|
223
|
+
subscribers.set(name, set);
|
|
249
224
|
set.add(cb);
|
|
225
|
+
follow(name);
|
|
250
226
|
return () => {
|
|
251
227
|
set.delete(cb);
|
|
252
228
|
if (set.size === 0) {
|
|
253
|
-
subscribers.delete(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
229
|
+
subscribers.delete(name);
|
|
230
|
+
loops.get(name)?.abort();
|
|
231
|
+
loops.delete(name);
|
|
257
232
|
}
|
|
258
233
|
};
|
|
259
234
|
},
|
|
260
235
|
async close() {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
if (socket.readyState === WS.OPEN || socket.readyState === WS.CONNECTING) {
|
|
266
|
-
socket.close();
|
|
267
|
-
}
|
|
236
|
+
for (const ctl of loops.values())
|
|
237
|
+
ctl.abort();
|
|
238
|
+
loops.clear();
|
|
268
239
|
subscribers.clear();
|
|
269
240
|
},
|
|
270
241
|
};
|
|
242
|
+
// Fail at creation, as the token mint did, when the engine refuses.
|
|
243
|
+
await provider.pull();
|
|
244
|
+
return provider;
|
|
271
245
|
}
|
package/dist/schema-codegen.d.ts
CHANGED
|
@@ -59,4 +59,26 @@ export interface CodegenOptions {
|
|
|
59
59
|
* `<extension>/.zveltio/db.d.ts`).
|
|
60
60
|
*/
|
|
61
61
|
export declare function emitTypeScript(schema: ParsedSchema, opts?: CodegenOptions): string;
|
|
62
|
+
/**
|
|
63
|
+
* TypeScript input type of each core field type. The engine registers the same
|
|
64
|
+
* types in `field-types/index.ts`; a unit test there keeps the two equal.
|
|
65
|
+
*/
|
|
66
|
+
export declare const CORE_FIELD_TS_TYPES: Readonly<Record<string, string>>;
|
|
67
|
+
export interface CollectionShape {
|
|
68
|
+
name: string;
|
|
69
|
+
fields: {
|
|
70
|
+
name: string;
|
|
71
|
+
type: string;
|
|
72
|
+
required?: boolean;
|
|
73
|
+
}[];
|
|
74
|
+
}
|
|
75
|
+
/** A type `tsTypeOf` does not know is `any`. */
|
|
76
|
+
export declare function emitCollectionTypes(collection: CollectionShape, tsTypeOf?: (type: string) => string | undefined): string;
|
|
77
|
+
/** The whole `zveltio.d.ts`. */
|
|
78
|
+
export declare function emitCollectionTypesFile(collections: CollectionShape[], tsTypeOf?: (type: string) => string | undefined): string;
|
|
79
|
+
/**
|
|
80
|
+
* The collections of a schema directory, as `{ path: content }` (the shape
|
|
81
|
+
* `zveltio schema pull` writes), in name order.
|
|
82
|
+
*/
|
|
83
|
+
export declare function collectionsFromSchemaFiles(files: Record<string, string>): CollectionShape[];
|
|
62
84
|
//# sourceMappingURL=schema-codegen.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-codegen.d.ts","sourceRoot":"","sources":["../src/schema-codegen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB;AAoND;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA+BtD;AA6BD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,YAAY,CAoE7D;AAID,MAAM,WAAW,cAAc;IAC7B,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,GAAE,cAAmB,GAAG,MAAM,CA0BtF"}
|
|
1
|
+
{"version":3,"file":"schema-codegen.d.ts","sourceRoot":"","sources":["../src/schema-codegen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,KAAK,EAAE,CAAC;CACjB;AAoND;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA+BtD;AA6BD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,YAAY,CAoE7D;AAID,MAAM,WAAW,cAAc;IAC7B,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,GAAE,cAAmB,GAAG,MAAM,CA0BtF;AAaD;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAuDhE,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;CAC9D;AAED,gDAAgD;AAChD,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,eAAe,EAC3B,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAyC,GAC7E,MAAM,CAeR;AAED,gCAAgC;AAChC,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,eAAe,EAAE,EAC9B,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAC9C,MAAM,CAGR;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,eAAe,EAAE,CAQ3F"}
|
package/dist/schema-codegen.js
CHANGED
|
@@ -401,3 +401,103 @@ export function emitTypeScript(schema, opts = {}) {
|
|
|
401
401
|
function quoteIdentifierIfNeeded(name) {
|
|
402
402
|
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "\\'")}'`;
|
|
403
403
|
}
|
|
404
|
+
// ── Collection types (RFC schema-as-code §8) ─────────────────────────────────
|
|
405
|
+
//
|
|
406
|
+
// One generator for both `zveltio generate-types` paths: the engine's
|
|
407
|
+
// `/api/admin/types` (which knows every registered field type, extensions'
|
|
408
|
+
// included) and `--from schema/` (no engine; core types only). Same fields in,
|
|
409
|
+
// same text out.
|
|
410
|
+
/**
|
|
411
|
+
* TypeScript input type of each core field type. The engine registers the same
|
|
412
|
+
* types in `field-types/index.ts`; a unit test there keeps the two equal.
|
|
413
|
+
*/
|
|
414
|
+
export const CORE_FIELD_TS_TYPES = {
|
|
415
|
+
text: 'string',
|
|
416
|
+
number: 'number',
|
|
417
|
+
boolean: 'boolean',
|
|
418
|
+
date: 'string',
|
|
419
|
+
datetime: 'string',
|
|
420
|
+
email: 'string',
|
|
421
|
+
url: 'string',
|
|
422
|
+
uuid: 'string',
|
|
423
|
+
json: 'Record<string, any>',
|
|
424
|
+
richtext: 'string',
|
|
425
|
+
file: '{ url: string; name: string; size: number; type: string }',
|
|
426
|
+
enum: 'string',
|
|
427
|
+
reference: 'string',
|
|
428
|
+
m2o: 'string',
|
|
429
|
+
location: '{ lat: number; lng: number }',
|
|
430
|
+
computed: 'never',
|
|
431
|
+
integer: 'number',
|
|
432
|
+
float: 'number',
|
|
433
|
+
textarea: 'string',
|
|
434
|
+
password: 'string',
|
|
435
|
+
slug: 'string',
|
|
436
|
+
color: 'string',
|
|
437
|
+
phone: 'string',
|
|
438
|
+
tags: 'string[]',
|
|
439
|
+
image: '{ url: string; name: string; size: number; width?: number; height?: number; alt?: string }',
|
|
440
|
+
o2m: 'string[]',
|
|
441
|
+
m2m: 'string[]',
|
|
442
|
+
m2a: '{ collection: string; id: string }[]',
|
|
443
|
+
geometry: '{ type: string; coordinates: any }',
|
|
444
|
+
vector: 'number[]',
|
|
445
|
+
smallint: 'number',
|
|
446
|
+
real: 'number',
|
|
447
|
+
decimal: 'number',
|
|
448
|
+
money: 'number',
|
|
449
|
+
varchar: 'string',
|
|
450
|
+
char: 'string',
|
|
451
|
+
time: 'string',
|
|
452
|
+
timetz: 'string',
|
|
453
|
+
interval: 'string',
|
|
454
|
+
bytea: 'string',
|
|
455
|
+
tsvector: 'string',
|
|
456
|
+
inet: 'string',
|
|
457
|
+
cidr: 'string',
|
|
458
|
+
macaddr: 'string',
|
|
459
|
+
xml: 'string',
|
|
460
|
+
bit: 'string',
|
|
461
|
+
varbit: 'string',
|
|
462
|
+
int4range: '{ lower: number; upper: number }',
|
|
463
|
+
int8range: '{ lower: number; upper: number }',
|
|
464
|
+
numrange: '{ lower: number; upper: number }',
|
|
465
|
+
daterange: '{ lower: string; upper: string }',
|
|
466
|
+
tsrange: '{ lower: string; upper: string }',
|
|
467
|
+
tstzrange: '{ lower: string; upper: string }',
|
|
468
|
+
};
|
|
469
|
+
/** A type `tsTypeOf` does not know is `any`. */
|
|
470
|
+
export function emitCollectionTypes(collection, tsTypeOf = (t) => CORE_FIELD_TS_TYPES[t]) {
|
|
471
|
+
const typeName = collection.name.charAt(0).toUpperCase() + collection.name.slice(1);
|
|
472
|
+
const inputFields = collection.fields
|
|
473
|
+
.map((f) => ` ${f.name}${f.required ? '' : '?'}: ${tsTypeOf(f.type) || 'any'};`)
|
|
474
|
+
.join('\n');
|
|
475
|
+
return `export interface ${typeName}Input {
|
|
476
|
+
${inputFields}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export interface ${typeName} extends ${typeName}Input {
|
|
480
|
+
id: string;
|
|
481
|
+
created_at: string;
|
|
482
|
+
updated_at: string;
|
|
483
|
+
status: string;
|
|
484
|
+
}`;
|
|
485
|
+
}
|
|
486
|
+
/** The whole `zveltio.d.ts`. */
|
|
487
|
+
export function emitCollectionTypesFile(collections, tsTypeOf) {
|
|
488
|
+
const types = collections.map((c) => emitCollectionTypes(c, tsTypeOf)).join('\n\n');
|
|
489
|
+
return `// Auto-generated by Zveltio\n// Do not edit manually\n\n${types}`;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* The collections of a schema directory, as `{ path: content }` (the shape
|
|
493
|
+
* `zveltio schema pull` writes), in name order.
|
|
494
|
+
*/
|
|
495
|
+
export function collectionsFromSchemaFiles(files) {
|
|
496
|
+
return Object.keys(files)
|
|
497
|
+
.filter((p) => /^collections\/[^/]+\.json$/.test(p))
|
|
498
|
+
.sort()
|
|
499
|
+
.map((p) => {
|
|
500
|
+
const file = JSON.parse(files[p]);
|
|
501
|
+
return { name: file.name, fields: file.fields ?? [] };
|
|
502
|
+
});
|
|
503
|
+
}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import type { Kysely } from 'kysely';
|
|
28
28
|
export interface WithTestDbOptions {
|
|
29
|
-
/** Postgres image. Default: `
|
|
29
|
+
/** Postgres image. Default: `pgvector/pgvector:pg18` (the image CI and docker-compose use). */
|
|
30
30
|
image?: string;
|
|
31
31
|
/** DB name to create. Default: `zveltio_test`. */
|
|
32
32
|
database?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"with-test-db.d.ts","sourceRoot":"","sources":["../../src/testing/with-test-db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGrC,MAAM,WAAW,iBAAiB;IAChC
|
|
1
|
+
{"version":3,"file":"with-test-db.d.ts","sourceRoot":"","sources":["../../src/testing/with-test-db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGrC,MAAM,WAAW,iBAAiB;IAChC,+FAA+F;IAC/F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,MAAM;IACrB,8DAA8D;IAE9D,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAChB,+CAA+C;IAC/C,gBAAgB,EAAE,MAAM,CAAC;IACzB,iEAAiE;IACjE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AA4BD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,IAAI,GAAE,iBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8E/E;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAEhC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,iBAAiB,EAE/D,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,CAAC,CAYZ;AAED;;;;;;;;;;;GAWG;AAGH,wBAAsB,qBAAqB,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBhG;AAED;;;GAGG;AAGH,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAIzF;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAStD;AAkBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAuErD;AAGD,eAAO,MAAM,iBAAiB;;CAAsB,CAAC"}
|
|
@@ -55,7 +55,7 @@ async function loadKysely() {
|
|
|
55
55
|
* pattern, prefer `withTestDb(fn)` below.
|
|
56
56
|
*/
|
|
57
57
|
export async function startTestDb(opts = {}) {
|
|
58
|
-
const image = opts.image ?? '
|
|
58
|
+
const image = opts.image ?? 'pgvector/pgvector:pg18';
|
|
59
59
|
const database = opts.database ?? `zveltio_test_${Date.now()}_${Math.floor(Math.random() * 10_000)}`;
|
|
60
60
|
const startupTimeoutMs = opts.startupTimeoutMs ?? 60_000;
|
|
61
61
|
const tc = await loadPgContainer();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-parse.test.d.ts","sourceRoot":"","sources":["../../src/tests/migration-parse.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { parseMigrationSql } from '../validate/migration-parse.js';
|
|
3
|
+
describe('parseMigrationSql', () => {
|
|
4
|
+
it('splits on a line that is exactly -- DOWN', () => {
|
|
5
|
+
expect(parseMigrationSql('CREATE TABLE a (id int);\n-- DOWN\nDROP TABLE a;')).toEqual({
|
|
6
|
+
up: 'CREATE TABLE a (id int);',
|
|
7
|
+
down: 'DROP TABLE a;',
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
it('does not end UP at a comment that only starts with -- DOWN', () => {
|
|
11
|
+
const raw = 'CREATE TABLE a (id int);\n-- DOWN: manual rollback required\nCREATE TABLE b (id int);';
|
|
12
|
+
expect(parseMigrationSql(raw)).toEqual({ up: raw, down: null });
|
|
13
|
+
});
|
|
14
|
+
it('accepts case and trailing spaces, like the engine', () => {
|
|
15
|
+
expect(parseMigrationSql('SELECT 1;\n-- down \nSELECT 2;').down).toBe('SELECT 2;');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-manifest-dependencies.test.d.ts","sourceRoot":"","sources":["../../src/tests/validate-manifest-dependencies.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// `zveltio extension validate` checks both dependency lists the broker reads.
|
|
2
|
+
import { describe, expect, it } from 'bun:test';
|
|
3
|
+
import { validateManifest } from '../validate/index.js';
|
|
4
|
+
const base = {
|
|
5
|
+
name: 'a',
|
|
6
|
+
displayName: 'A',
|
|
7
|
+
category: 'custom',
|
|
8
|
+
description: 'd',
|
|
9
|
+
version: '1.0.0',
|
|
10
|
+
};
|
|
11
|
+
const codes = (extra) => validateManifest({ manifest: { ...base, ...extra } })
|
|
12
|
+
.filter((e) => e.code === 'MANIFEST_BAD_DEPENDENCY')
|
|
13
|
+
.map((e) => e.message);
|
|
14
|
+
describe('manifest dependencies / optionalDependencies', () => {
|
|
15
|
+
it('accepts both lists', () => {
|
|
16
|
+
expect(codes({
|
|
17
|
+
dependencies: [{ name: 'crm', minVersion: '1.0.0' }],
|
|
18
|
+
optionalDependencies: [{ name: 'ai' }],
|
|
19
|
+
})).toEqual([]);
|
|
20
|
+
});
|
|
21
|
+
it('refuses a malformed entry, a bad minVersion and a name in both lists', () => {
|
|
22
|
+
expect(codes({ optionalDependencies: 'ai' })).toHaveLength(1);
|
|
23
|
+
expect(codes({ optionalDependencies: [{ minVersion: '1.0.0' }] })[0]).toContain('has no name');
|
|
24
|
+
expect(codes({ optionalDependencies: [{ name: 'ai', minVersion: 'x' }] })[0]).toContain('not semver');
|
|
25
|
+
expect(codes({ dependencies: [{ name: 'ai' }], optionalDependencies: [{ name: 'ai' }] })[0]).toContain('"ai" is listed in "dependencies" and "optionalDependencies"');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-manifest-forward-credentials.test.d.ts","sourceRoot":"","sources":["../../src/tests/validate-manifest-forward-credentials.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// `zveltio extension validate` refuses the forwardCredentials the engine would refuse.
|
|
2
|
+
import { describe, expect, it } from 'bun:test';
|
|
3
|
+
import { validateManifest } from '../validate/index.js';
|
|
4
|
+
const base = {
|
|
5
|
+
name: 'a',
|
|
6
|
+
displayName: 'A',
|
|
7
|
+
category: 'custom',
|
|
8
|
+
description: 'd',
|
|
9
|
+
version: '1.0.0',
|
|
10
|
+
publicRoutes: ['/scim/*'],
|
|
11
|
+
};
|
|
12
|
+
const errors = (forwardCredentials) => validateManifest({ manifest: { ...base, forwardCredentials } }).filter((e) => e.code === 'MANIFEST_BAD_FORWARD_CREDENTIALS');
|
|
13
|
+
describe('manifest forwardCredentials', () => {
|
|
14
|
+
it('accepts a credential header on a public route', () => {
|
|
15
|
+
expect(errors({ '/scim/*': ['authorization'] })).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
it('refuses an unknown header, a non-public route and a non-object', () => {
|
|
18
|
+
expect(errors({ '/scim/*': ['stripe-signature'] })).toHaveLength(1);
|
|
19
|
+
expect(errors({ '/admin/*': ['cookie'] })).toHaveLength(1);
|
|
20
|
+
expect(errors(['authorization'])).toHaveLength(1);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/validate/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/validate/index.ts"],"names":[],"mappings":"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"}
|