@zveltio/sdk 2.0.8 → 3.0.0-beta.1
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/build/index.d.ts +77 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +157 -0
- package/dist/client/QueryBuilder.d.ts.map +1 -1
- package/dist/client/QueryBuilder.js +12 -3
- package/dist/client/RealtimeClient.d.ts.map +1 -1
- package/dist/client/RealtimeClient.js +3 -1
- package/dist/client/ZveltioClient.d.ts.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +16 -5
- package/dist/core.d.ts.map +1 -1
- package/dist/ddl/index.d.ts +153 -0
- package/dist/ddl/index.d.ts.map +1 -0
- package/dist/ddl/index.js +33 -0
- package/dist/extension/index.d.ts +522 -10
- package/dist/extension/index.d.ts.map +1 -1
- package/dist/extension/index.js +1 -1
- package/dist/extension/permission-gate.d.ts +37 -0
- package/dist/extension/permission-gate.d.ts.map +1 -0
- package/dist/extension/permission-gate.js +93 -0
- package/dist/generate-types.d.ts +37 -0
- package/dist/generate-types.d.ts.map +1 -0
- package/dist/generate-types.js +157 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/local-store.d.ts.map +1 -1
- package/dist/local-store.js +1 -3
- package/dist/offline/index.d.ts +105 -0
- package/dist/offline/index.d.ts.map +1 -0
- package/dist/offline/index.js +235 -0
- package/dist/publish/index.d.ts +94 -0
- package/dist/publish/index.d.ts.map +1 -0
- package/dist/publish/index.js +148 -0
- package/dist/realtime.d.ts.map +1 -1
- package/dist/realtime.js +6 -2
- package/dist/rpc/index.d.ts +68 -0
- package/dist/rpc/index.d.ts.map +1 -0
- package/dist/rpc/index.js +61 -0
- package/dist/schema-codegen.d.ts +60 -0
- package/dist/schema-codegen.d.ts.map +1 -0
- package/dist/schema-codegen.js +369 -0
- package/dist/schema-watcher.d.ts.map +1 -1
- package/dist/schema-watcher.js +1 -3
- package/dist/studio/index.d.ts +84 -0
- package/dist/studio/index.d.ts.map +1 -0
- package/dist/studio/index.js +180 -0
- package/dist/svelte.d.ts.map +1 -1
- package/dist/sync-manager.d.ts.map +1 -1
- package/dist/sync-manager.js +4 -11
- package/dist/testing/index.d.ts +121 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +283 -0
- package/dist/testing/with-test-db.d.ts +119 -0
- package/dist/testing/with-test-db.d.ts.map +1 -0
- package/dist/testing/with-test-db.js +313 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +3 -1
- package/dist/validate/index.d.ts +95 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +201 -0
- package/dist/validate/migration-parse.d.ts +17 -0
- package/dist/validate/migration-parse.d.ts.map +1 -0
- package/dist/validate/migration-parse.js +11 -0
- package/package.json +53 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres-backed integration test helper (S4-06 follow-up).
|
|
3
|
+
*
|
|
4
|
+
* `withTestDb(fn)` spins up a real Postgres container via `testcontainers`,
|
|
5
|
+
* yields a Kysely instance connected to it, runs the user's callback, then
|
|
6
|
+
* tears the container down. Use this when `mockDb` is insufficient — e.g.
|
|
7
|
+
* when an extension hook chains a real SELECT through `ctx.db`, or when you
|
|
8
|
+
* want to verify migrations apply cleanly.
|
|
9
|
+
*
|
|
10
|
+
* Requires:
|
|
11
|
+
* - Docker / Podman running on the test host.
|
|
12
|
+
* - `testcontainers` installed (peer dep — declare as devDep in the
|
|
13
|
+
* extension's package.json). We import it dynamically so unit-test
|
|
14
|
+
* workflows that never call `withTestDb` don't take the heavy hit.
|
|
15
|
+
*
|
|
16
|
+
* Cost model:
|
|
17
|
+
* - First call: ~3-5s (image pull + container start). Subsequent calls
|
|
18
|
+
* reuse the cached image, ~1-2s. Use one container across a `describe()`
|
|
19
|
+
* block when you can — the wrapper supports nesting via `reuse`.
|
|
20
|
+
*
|
|
21
|
+
* Why a peer dep + dynamic import:
|
|
22
|
+
* - `testcontainers` pulls `dockerode` + a bunch of Node-ish modules. We
|
|
23
|
+
* don't want every consumer of `@zveltio/sdk/testing` to inherit that
|
|
24
|
+
* weight just to use `mockDb`. By importing lazily, the cost is
|
|
25
|
+
* opt-in at the call site.
|
|
26
|
+
*/
|
|
27
|
+
import type { Kysely } from 'kysely';
|
|
28
|
+
export interface WithTestDbOptions {
|
|
29
|
+
/** Postgres image. Default: `postgres:18-alpine`. */
|
|
30
|
+
image?: string;
|
|
31
|
+
/** DB name to create. Default: `zveltio_test`. */
|
|
32
|
+
database?: string;
|
|
33
|
+
/** Optional SQL strings to run after the container is ready. */
|
|
34
|
+
migrations?: string[];
|
|
35
|
+
/**
|
|
36
|
+
* Container start timeout, ms. Default: 60_000. Cold pulls can be slow
|
|
37
|
+
* on the first run; CI environments may want 120_000.
|
|
38
|
+
*/
|
|
39
|
+
startupTimeoutMs?: number;
|
|
40
|
+
/**
|
|
41
|
+
* If passed, the container is started once and reused. Pass `false`
|
|
42
|
+
* (default) to start + stop per call.
|
|
43
|
+
*/
|
|
44
|
+
reuse?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface TestDb {
|
|
47
|
+
/** Kysely instance — same shape as `ctx.db` in production. */
|
|
48
|
+
db: Kysely<any>;
|
|
49
|
+
/** Connection string the container exposed. */
|
|
50
|
+
connectionString: string;
|
|
51
|
+
/** Stop + remove the container (also closes the Kysely pool). */
|
|
52
|
+
cleanup(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Spin up Postgres, optionally run migrations, return a `TestDb` handle.
|
|
56
|
+
* The caller is responsible for calling `cleanup()` (typically inside an
|
|
57
|
+
* `afterAll`/`afterEach`).
|
|
58
|
+
*
|
|
59
|
+
* For the more common "scope the lifecycle to a test/describe block"
|
|
60
|
+
* pattern, prefer `withTestDb(fn)` below.
|
|
61
|
+
*/
|
|
62
|
+
export declare function startTestDb(opts?: WithTestDbOptions): Promise<TestDb>;
|
|
63
|
+
/**
|
|
64
|
+
* Convenience wrapper: starts a TestDb, runs `fn`, always cleans up. The
|
|
65
|
+
* 95% case for integration tests. Returns whatever `fn` returns.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* import { withTestDb } from '@zveltio/sdk/testing';
|
|
69
|
+
* import myExtension from '../engine';
|
|
70
|
+
*
|
|
71
|
+
* test('extension creates contact rows', async () => {
|
|
72
|
+
* await withTestDb(async (db) => {
|
|
73
|
+
* await db.schema.createTable('zvd_contacts').addColumn(...).execute();
|
|
74
|
+
* const ctx = createTestContext({ db });
|
|
75
|
+
* const app = await createTestApp(myExtension, { ctx });
|
|
76
|
+
* const res = await app.request('/ext/my/contacts', { method: 'POST', ... });
|
|
77
|
+
* expect(res.status).toBe(201);
|
|
78
|
+
* const rows = await db.selectFrom('zvd_contacts').selectAll().execute();
|
|
79
|
+
* expect(rows).toHaveLength(1);
|
|
80
|
+
* });
|
|
81
|
+
* });
|
|
82
|
+
*/
|
|
83
|
+
export declare function withTestDb<T>(fnOrOpts: ((db: Kysely<any>) => Promise<T>) | WithTestDbOptions, maybeFn?: (db: Kysely<any>) => Promise<T>): Promise<T>;
|
|
84
|
+
/**
|
|
85
|
+
* Apply an ordered list of SQL strings against the db. Each entry runs in
|
|
86
|
+
* its own statement; semicolons in the source string split into multiple
|
|
87
|
+
* statements. Useful for replaying engine + extension migrations against
|
|
88
|
+
* the test db.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* import { readFileSync } from 'fs';
|
|
92
|
+
* import { sync as glob } from 'glob';
|
|
93
|
+
* const files = glob('engine/migrations/*.sql').map((p) => readFileSync(p, 'utf8'));
|
|
94
|
+
* await applyMigrationStrings(db, files);
|
|
95
|
+
*/
|
|
96
|
+
export declare function applyMigrationStrings(db: Kysely<any>, sqlStrings: string[]): Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Same as `applyMigrationStrings` but takes file paths. Reads each file
|
|
99
|
+
* synchronously, applies in order. Bun-native — uses `Bun.file()`.
|
|
100
|
+
*/
|
|
101
|
+
export declare function applyMigrationFiles(db: Kysely<any>, paths: string[]): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Stop any reuse-cached container. Call this from a `globalTeardown` /
|
|
104
|
+
* `afterAll` so test runs don't leave dangling containers. No-op when no
|
|
105
|
+
* reuse container is active.
|
|
106
|
+
*/
|
|
107
|
+
export declare function stopReusedTestDb(): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Naive SQL statement splitter: splits on `;` outside of single-quoted
|
|
110
|
+
* strings and dollar-quoted blocks (`$$...$$`). Handles the migration-file
|
|
111
|
+
* styles Zveltio ships; not a full SQL parser.
|
|
112
|
+
*
|
|
113
|
+
* Public so tests can pin its behavior. Not part of the documented surface.
|
|
114
|
+
*/
|
|
115
|
+
export declare function splitStatements(src: string): string[];
|
|
116
|
+
export declare const _internalForTests: {
|
|
117
|
+
splitStatements: typeof splitStatements;
|
|
118
|
+
};
|
|
119
|
+
//# sourceMappingURL=with-test-db.d.ts.map
|
|
@@ -0,0 +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;AAErC,MAAM,WAAW,iBAAiB;IAChC,qDAAqD;IACrD,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;IAC9D,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;AA6BD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,IAAI,GAAE,iBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CA4E/E;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAChC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,iBAAiB,EAC/D,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,CAAC,CAYZ;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhG;AAED;;;GAGG;AACH,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,CAiBtD;AAgBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAuErD;AAGD,eAAO,MAAM,iBAAiB;;CAAsB,CAAC"}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres-backed integration test helper (S4-06 follow-up).
|
|
3
|
+
*
|
|
4
|
+
* `withTestDb(fn)` spins up a real Postgres container via `testcontainers`,
|
|
5
|
+
* yields a Kysely instance connected to it, runs the user's callback, then
|
|
6
|
+
* tears the container down. Use this when `mockDb` is insufficient — e.g.
|
|
7
|
+
* when an extension hook chains a real SELECT through `ctx.db`, or when you
|
|
8
|
+
* want to verify migrations apply cleanly.
|
|
9
|
+
*
|
|
10
|
+
* Requires:
|
|
11
|
+
* - Docker / Podman running on the test host.
|
|
12
|
+
* - `testcontainers` installed (peer dep — declare as devDep in the
|
|
13
|
+
* extension's package.json). We import it dynamically so unit-test
|
|
14
|
+
* workflows that never call `withTestDb` don't take the heavy hit.
|
|
15
|
+
*
|
|
16
|
+
* Cost model:
|
|
17
|
+
* - First call: ~3-5s (image pull + container start). Subsequent calls
|
|
18
|
+
* reuse the cached image, ~1-2s. Use one container across a `describe()`
|
|
19
|
+
* block when you can — the wrapper supports nesting via `reuse`.
|
|
20
|
+
*
|
|
21
|
+
* Why a peer dep + dynamic import:
|
|
22
|
+
* - `testcontainers` pulls `dockerode` + a bunch of Node-ish modules. We
|
|
23
|
+
* don't want every consumer of `@zveltio/sdk/testing` to inherit that
|
|
24
|
+
* weight just to use `mockDb`. By importing lazily, the cost is
|
|
25
|
+
* opt-in at the call site.
|
|
26
|
+
*/
|
|
27
|
+
// ── Module-level reuse cache ────────────────────────────────────────────────
|
|
28
|
+
//
|
|
29
|
+
// When `reuse: true`, we keep a single container alive for the process
|
|
30
|
+
// lifetime. The first call to `withTestDb` boots it; subsequent calls open
|
|
31
|
+
// fresh DBs *inside* the same container (CREATE DATABASE per-call), so
|
|
32
|
+
// tests stay isolated without paying the container-start cost again.
|
|
33
|
+
let _sharedContainer = null;
|
|
34
|
+
let _sharedClient = null;
|
|
35
|
+
let _sharedDbCounter = 0;
|
|
36
|
+
async function loadPgContainer() {
|
|
37
|
+
try {
|
|
38
|
+
return await import('@testcontainers/postgresql');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error(`withTestDb requires the '@testcontainers/postgresql' npm package. ` +
|
|
42
|
+
`Install it as a devDep in your extension:\n` +
|
|
43
|
+
` bun add -d @testcontainers/postgresql pg @types/pg`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function loadKysely() {
|
|
47
|
+
return await import('kysely');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Spin up Postgres, optionally run migrations, return a `TestDb` handle.
|
|
51
|
+
* The caller is responsible for calling `cleanup()` (typically inside an
|
|
52
|
+
* `afterAll`/`afterEach`).
|
|
53
|
+
*
|
|
54
|
+
* For the more common "scope the lifecycle to a test/describe block"
|
|
55
|
+
* pattern, prefer `withTestDb(fn)` below.
|
|
56
|
+
*/
|
|
57
|
+
export async function startTestDb(opts = {}) {
|
|
58
|
+
const image = opts.image ?? 'postgres:18-alpine';
|
|
59
|
+
const database = opts.database ?? `zveltio_test_${Date.now()}_${Math.floor(Math.random() * 10_000)}`;
|
|
60
|
+
const startupTimeoutMs = opts.startupTimeoutMs ?? 60_000;
|
|
61
|
+
const tc = await loadPgContainer();
|
|
62
|
+
const { Kysely, PostgresDialect } = await loadKysely();
|
|
63
|
+
let host;
|
|
64
|
+
let port;
|
|
65
|
+
let username;
|
|
66
|
+
let password;
|
|
67
|
+
let createdContainer = null;
|
|
68
|
+
if (opts.reuse && _sharedContainer) {
|
|
69
|
+
host = _sharedContainer.getHost();
|
|
70
|
+
port = _sharedContainer.getMappedPort(5432);
|
|
71
|
+
username = _sharedContainer.getUsername();
|
|
72
|
+
password = _sharedContainer.getPassword();
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const builder = new tc.PostgreSqlContainer(image).withStartupTimeout(startupTimeoutMs);
|
|
76
|
+
createdContainer = await builder.start();
|
|
77
|
+
if (opts.reuse)
|
|
78
|
+
_sharedContainer = createdContainer;
|
|
79
|
+
host = createdContainer.getHost();
|
|
80
|
+
port = createdContainer.getMappedPort(5432);
|
|
81
|
+
username = createdContainer.getUsername();
|
|
82
|
+
password = createdContainer.getPassword();
|
|
83
|
+
}
|
|
84
|
+
// testcontainers' default db is `test`. Create our own so multiple
|
|
85
|
+
// reuse=true calls don't share state.
|
|
86
|
+
const adminConnStr = `postgres://${username}:${password}@${host}:${port}/postgres`;
|
|
87
|
+
const adminPool = await openPgPool(adminConnStr);
|
|
88
|
+
try {
|
|
89
|
+
await adminPool.query(`CREATE DATABASE "${database}"`);
|
|
90
|
+
_sharedDbCounter++;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await adminPool.end();
|
|
94
|
+
}
|
|
95
|
+
const connectionString = `postgres://${username}:${password}@${host}:${port}/${database}`;
|
|
96
|
+
const pool = await openPgPool(connectionString);
|
|
97
|
+
const db = new Kysely({
|
|
98
|
+
dialect: new PostgresDialect({ pool: pool }),
|
|
99
|
+
});
|
|
100
|
+
if (opts.migrations && opts.migrations.length > 0) {
|
|
101
|
+
await applyMigrationStrings(db, opts.migrations);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
db,
|
|
105
|
+
connectionString,
|
|
106
|
+
async cleanup() {
|
|
107
|
+
try {
|
|
108
|
+
await db.destroy();
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
/* */
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
await pool.end();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* */
|
|
118
|
+
}
|
|
119
|
+
if (createdContainer && !opts.reuse) {
|
|
120
|
+
try {
|
|
121
|
+
await createdContainer.stop();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* */
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Convenience wrapper: starts a TestDb, runs `fn`, always cleans up. The
|
|
132
|
+
* 95% case for integration tests. Returns whatever `fn` returns.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* import { withTestDb } from '@zveltio/sdk/testing';
|
|
136
|
+
* import myExtension from '../engine';
|
|
137
|
+
*
|
|
138
|
+
* test('extension creates contact rows', async () => {
|
|
139
|
+
* await withTestDb(async (db) => {
|
|
140
|
+
* await db.schema.createTable('zvd_contacts').addColumn(...).execute();
|
|
141
|
+
* const ctx = createTestContext({ db });
|
|
142
|
+
* const app = await createTestApp(myExtension, { ctx });
|
|
143
|
+
* const res = await app.request('/ext/my/contacts', { method: 'POST', ... });
|
|
144
|
+
* expect(res.status).toBe(201);
|
|
145
|
+
* const rows = await db.selectFrom('zvd_contacts').selectAll().execute();
|
|
146
|
+
* expect(rows).toHaveLength(1);
|
|
147
|
+
* });
|
|
148
|
+
* });
|
|
149
|
+
*/
|
|
150
|
+
export async function withTestDb(fnOrOpts, maybeFn) {
|
|
151
|
+
const opts = typeof fnOrOpts === 'function' ? {} : fnOrOpts;
|
|
152
|
+
const fn = typeof fnOrOpts === 'function' ? fnOrOpts : maybeFn;
|
|
153
|
+
if (typeof fn !== 'function') {
|
|
154
|
+
throw new Error('withTestDb: callback is required');
|
|
155
|
+
}
|
|
156
|
+
const handle = await startTestDb(opts);
|
|
157
|
+
try {
|
|
158
|
+
return await fn(handle.db);
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
await handle.cleanup();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Apply an ordered list of SQL strings against the db. Each entry runs in
|
|
166
|
+
* its own statement; semicolons in the source string split into multiple
|
|
167
|
+
* statements. Useful for replaying engine + extension migrations against
|
|
168
|
+
* the test db.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* import { readFileSync } from 'fs';
|
|
172
|
+
* import { sync as glob } from 'glob';
|
|
173
|
+
* const files = glob('engine/migrations/*.sql').map((p) => readFileSync(p, 'utf8'));
|
|
174
|
+
* await applyMigrationStrings(db, files);
|
|
175
|
+
*/
|
|
176
|
+
export async function applyMigrationStrings(db, sqlStrings) {
|
|
177
|
+
const { sql } = await loadKysely();
|
|
178
|
+
for (const raw of sqlStrings) {
|
|
179
|
+
const statements = splitStatements(raw);
|
|
180
|
+
for (const stmt of statements) {
|
|
181
|
+
const trimmed = stmt.trim();
|
|
182
|
+
if (!trimmed)
|
|
183
|
+
continue;
|
|
184
|
+
await sql.raw(trimmed).execute(db);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Same as `applyMigrationStrings` but takes file paths. Reads each file
|
|
190
|
+
* synchronously, applies in order. Bun-native — uses `Bun.file()`.
|
|
191
|
+
*/
|
|
192
|
+
export async function applyMigrationFiles(db, paths) {
|
|
193
|
+
// Bun.file().text() returns a Promise<string>.
|
|
194
|
+
const contents = await Promise.all(paths.map((p) => Bun.file(p).text()));
|
|
195
|
+
return applyMigrationStrings(db, contents);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Stop any reuse-cached container. Call this from a `globalTeardown` /
|
|
199
|
+
* `afterAll` so test runs don't leave dangling containers. No-op when no
|
|
200
|
+
* reuse container is active.
|
|
201
|
+
*/
|
|
202
|
+
export async function stopReusedTestDb() {
|
|
203
|
+
if (_sharedClient) {
|
|
204
|
+
try {
|
|
205
|
+
await _sharedClient.end();
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
/* */
|
|
209
|
+
}
|
|
210
|
+
_sharedClient = null;
|
|
211
|
+
}
|
|
212
|
+
if (_sharedContainer) {
|
|
213
|
+
try {
|
|
214
|
+
await _sharedContainer.stop();
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
/* */
|
|
218
|
+
}
|
|
219
|
+
_sharedContainer = null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// ── Internals ───────────────────────────────────────────────────────────────
|
|
223
|
+
/** Open a pg connection pool. Uses `pg` (peer dep with testcontainers). */
|
|
224
|
+
async function openPgPool(connectionString) {
|
|
225
|
+
const pg = await import('pg').catch(() => {
|
|
226
|
+
throw new Error(`withTestDb requires the 'pg' npm package. Install it alongside testcontainers:\n` +
|
|
227
|
+
` bun add -d testcontainers pg @types/pg`);
|
|
228
|
+
});
|
|
229
|
+
// pg's Pool — the Kysely PostgresDialect accepts this directly.
|
|
230
|
+
return new pg.Pool({ connectionString });
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Naive SQL statement splitter: splits on `;` outside of single-quoted
|
|
234
|
+
* strings and dollar-quoted blocks (`$$...$$`). Handles the migration-file
|
|
235
|
+
* styles Zveltio ships; not a full SQL parser.
|
|
236
|
+
*
|
|
237
|
+
* Public so tests can pin its behavior. Not part of the documented surface.
|
|
238
|
+
*/
|
|
239
|
+
export function splitStatements(src) {
|
|
240
|
+
const out = [];
|
|
241
|
+
let buf = '';
|
|
242
|
+
let i = 0;
|
|
243
|
+
while (i < src.length) {
|
|
244
|
+
const ch = src[i];
|
|
245
|
+
// Comments: -- to end of line
|
|
246
|
+
if (ch === '-' && src[i + 1] === '-') {
|
|
247
|
+
while (i < src.length && src[i] !== '\n') {
|
|
248
|
+
buf += src[i++];
|
|
249
|
+
}
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
// Block comments: /* ... */
|
|
253
|
+
if (ch === '/' && src[i + 1] === '*') {
|
|
254
|
+
buf += src[i++];
|
|
255
|
+
buf += src[i++];
|
|
256
|
+
while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) {
|
|
257
|
+
buf += src[i++];
|
|
258
|
+
}
|
|
259
|
+
if (i < src.length) {
|
|
260
|
+
buf += src[i++];
|
|
261
|
+
buf += src[i++];
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
// Single-quoted strings: handle escaped quotes.
|
|
266
|
+
if (ch === "'") {
|
|
267
|
+
buf += src[i++];
|
|
268
|
+
while (i < src.length) {
|
|
269
|
+
if (src[i] === "'" && src[i + 1] === "'") {
|
|
270
|
+
buf += src[i++];
|
|
271
|
+
buf += src[i++];
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (src[i] === "'") {
|
|
275
|
+
buf += src[i++];
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
buf += src[i++];
|
|
279
|
+
}
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
// Dollar-quoted blocks: $$ ... $$ or $tag$ ... $tag$.
|
|
283
|
+
if (ch === '$') {
|
|
284
|
+
const tagMatch = src.slice(i).match(/^\$[a-zA-Z0-9_]*\$/);
|
|
285
|
+
if (tagMatch) {
|
|
286
|
+
const tag = tagMatch[0];
|
|
287
|
+
buf += tag;
|
|
288
|
+
i += tag.length;
|
|
289
|
+
const endIdx = src.indexOf(tag, i);
|
|
290
|
+
if (endIdx < 0) {
|
|
291
|
+
buf += src.slice(i);
|
|
292
|
+
i = src.length;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
buf += src.slice(i, endIdx + tag.length);
|
|
296
|
+
i = endIdx + tag.length;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (ch === ';') {
|
|
301
|
+
out.push(buf);
|
|
302
|
+
buf = '';
|
|
303
|
+
i++;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
buf += src[i++];
|
|
307
|
+
}
|
|
308
|
+
if (buf.trim())
|
|
309
|
+
out.push(buf);
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
// ── Internal helpers for tests only ────────────────────────────────────────
|
|
313
|
+
export const _internalForTests = { splitStatements };
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,YAAY,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAYrC"}
|
package/dist/utils.js
CHANGED
|
@@ -14,6 +14,8 @@ export function generateUUID() {
|
|
|
14
14
|
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
15
15
|
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
|
|
16
16
|
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant bits
|
|
17
|
-
const hex = Array.from(bytes)
|
|
17
|
+
const hex = Array.from(bytes)
|
|
18
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
19
|
+
.join('');
|
|
18
20
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
19
21
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-publish validators for Zveltio extensions (S4-04).
|
|
3
|
+
*
|
|
4
|
+
* Pure functions — no Node-specific imports, no Zod runtime. Designed so the
|
|
5
|
+
* CLI (`zveltio extension validate`) can call them on file contents read from
|
|
6
|
+
* disk, the engine can call them on uploaded archives, and registry code can
|
|
7
|
+
* call them on publish. Each returns a `ValidationResult` with structured
|
|
8
|
+
* diagnostics instead of throwing, so the CLI can print all problems at once.
|
|
9
|
+
*
|
|
10
|
+
* What's checked:
|
|
11
|
+
* - manifest.json shape (required fields, semver, sane category)
|
|
12
|
+
* - name matches the folder path it lives in
|
|
13
|
+
* - peerDependencies appear on the platform allow-list
|
|
14
|
+
* - migration SQL files parse + table coverage looks sane
|
|
15
|
+
* - destructive DDL in migrations has a `-- DOWN` section
|
|
16
|
+
* - bundle size stays within quota
|
|
17
|
+
*
|
|
18
|
+
* Out of scope (deferred):
|
|
19
|
+
* - Loading the extension module to inspect its default export. Requires
|
|
20
|
+
* a Bun runtime + sandbox. CLI's `validate` cannot guarantee correctness
|
|
21
|
+
* for runtime contracts; that's what `extension types` (S4-01) + `tsc`
|
|
22
|
+
* gives you locally.
|
|
23
|
+
* - Building the Studio bundle. Too slow; pair with `zveltio extension build`
|
|
24
|
+
* which can call validate as a sub-step.
|
|
25
|
+
*/
|
|
26
|
+
export interface ValidationError {
|
|
27
|
+
/** Stable machine-readable code (e.g. `MANIFEST_NAME_MISMATCH`). */
|
|
28
|
+
code: string;
|
|
29
|
+
/** Operator-facing message. */
|
|
30
|
+
message: string;
|
|
31
|
+
/** Optional file path the error applies to, relative to the extension root. */
|
|
32
|
+
file?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ValidationResult {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
errors: ValidationError[];
|
|
37
|
+
/** Counts surfaced for human-readable summaries. */
|
|
38
|
+
stats: {
|
|
39
|
+
tables: number;
|
|
40
|
+
migrations: number;
|
|
41
|
+
peerDeps: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export interface ManifestValidationInput {
|
|
45
|
+
/** Parsed JSON object (or `null` if file couldn't be read). */
|
|
46
|
+
manifest: unknown;
|
|
47
|
+
/** Folder path slug — e.g. `'finance/invoicing'`. Used for name-match. */
|
|
48
|
+
expectedName?: string;
|
|
49
|
+
}
|
|
50
|
+
export declare function validateManifest(input: ManifestValidationInput): ValidationError[];
|
|
51
|
+
export interface PeerDepsValidationInput {
|
|
52
|
+
/** From manifest.json: `{ "lodash": "^4.0.0", ... }` (or undefined). */
|
|
53
|
+
peerDependencies: Record<string, string> | undefined;
|
|
54
|
+
/** The platform allow-list (caller supplies it — see peer-deps-allowlist.ts). */
|
|
55
|
+
allowedPackages: ReadonlySet<string>;
|
|
56
|
+
}
|
|
57
|
+
export declare function validatePeerDependencies(input: PeerDepsValidationInput): ValidationError[];
|
|
58
|
+
export interface MigrationsValidationInput {
|
|
59
|
+
/** Pairs of (filename, sql contents). Filename is just for diagnostics. */
|
|
60
|
+
files: Array<{
|
|
61
|
+
filename: string;
|
|
62
|
+
sql: string;
|
|
63
|
+
}>;
|
|
64
|
+
/** When `true`, every migration with destructive DDL must have a -- DOWN section. */
|
|
65
|
+
requireDownForDestructive?: boolean;
|
|
66
|
+
}
|
|
67
|
+
export declare function validateMigrations(input: MigrationsValidationInput): ValidationError[];
|
|
68
|
+
export interface FilePresenceInput {
|
|
69
|
+
/** Mapping of relative path → existence boolean. Caller does the disk hit. */
|
|
70
|
+
paths: Record<string, boolean>;
|
|
71
|
+
/** Paths that MUST exist. */
|
|
72
|
+
required: string[];
|
|
73
|
+
}
|
|
74
|
+
export declare function validateFilePresence(input: FilePresenceInput): ValidationError[];
|
|
75
|
+
export interface BundleSizeInput {
|
|
76
|
+
/** Size of the extension folder (excluding node_modules), in bytes. */
|
|
77
|
+
bundleBytes: number;
|
|
78
|
+
/** Override the default cap (50 MB). */
|
|
79
|
+
bundleSizeKbMax?: number;
|
|
80
|
+
}
|
|
81
|
+
export declare function validateBundleSize(input: BundleSizeInput): ValidationError[];
|
|
82
|
+
export interface ValidateExtensionInput {
|
|
83
|
+
manifest: ManifestValidationInput;
|
|
84
|
+
peerDeps: PeerDepsValidationInput;
|
|
85
|
+
migrations: MigrationsValidationInput;
|
|
86
|
+
filePresence: FilePresenceInput;
|
|
87
|
+
bundleSize?: BundleSizeInput;
|
|
88
|
+
/** Numbers used to fill the result's `stats` for human summaries. */
|
|
89
|
+
stats: {
|
|
90
|
+
tables: number;
|
|
91
|
+
migrations: number;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export declare function validateExtension(input: ValidateExtensionInput): ValidationResult;
|
|
95
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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;CACf;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;AA+CD,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,CA2DlF;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;AAkBD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,yBAAyB,GAAG,eAAe,EAAE,CAqCtF;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;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,CAiBjF"}
|