dzql 0.5.33 → 0.6.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/.env.sample +28 -0
- package/compose.yml +28 -0
- package/dist/client/index.ts +1 -0
- package/dist/client/stores/useMyProfileStore.ts +114 -0
- package/dist/client/stores/useOrgDashboardStore.ts +131 -0
- package/dist/client/stores/useVenueDetailStore.ts +117 -0
- package/dist/client/ws.ts +716 -0
- package/dist/db/migrations/000_core.sql +92 -0
- package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
- package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
- package/dist/runtime/manifest.json +1562 -0
- package/docs/README.md +309 -36
- package/docs/feature-requests/applyPatch-bug-report.md +85 -0
- package/docs/feature-requests/connection-ready-profile.md +57 -0
- package/docs/feature-requests/hidden-bug-report.md +111 -0
- package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
- package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
- package/docs/feature-requests/todo.md +146 -0
- package/docs/for_ai.md +653 -0
- package/docs/project-setup.md +456 -0
- package/examples/blog.ts +50 -0
- package/examples/invalid.ts +18 -0
- package/examples/venues.js +485 -0
- package/package.json +23 -60
- package/src/cli/codegen/client.ts +99 -0
- package/src/cli/codegen/manifest.ts +95 -0
- package/src/cli/codegen/pinia.ts +174 -0
- package/src/cli/codegen/realtime.ts +58 -0
- package/src/cli/codegen/sql.ts +698 -0
- package/src/cli/codegen/subscribable_sql.ts +547 -0
- package/src/cli/codegen/subscribable_store.ts +184 -0
- package/src/cli/codegen/types.ts +142 -0
- package/src/cli/compiler/analyzer.ts +52 -0
- package/src/cli/compiler/graph_rules.ts +251 -0
- package/src/cli/compiler/ir.ts +233 -0
- package/src/cli/compiler/loader.ts +132 -0
- package/src/cli/compiler/permissions.ts +227 -0
- package/src/cli/index.ts +166 -0
- package/src/client/index.ts +1 -0
- package/src/client/ws.ts +286 -0
- package/src/runtime/auth.ts +39 -0
- package/src/runtime/db.ts +33 -0
- package/src/runtime/errors.ts +51 -0
- package/src/runtime/index.ts +98 -0
- package/src/runtime/js_functions.ts +63 -0
- package/src/runtime/manifest_loader.ts +29 -0
- package/src/runtime/namespace.ts +483 -0
- package/src/runtime/server.ts +87 -0
- package/src/runtime/ws.ts +197 -0
- package/src/shared/ir.ts +197 -0
- package/tests/client.test.ts +38 -0
- package/tests/codegen.test.ts +71 -0
- package/tests/compiler.test.ts +45 -0
- package/tests/graph_rules.test.ts +173 -0
- package/tests/integration/db.test.ts +174 -0
- package/tests/integration/e2e.test.ts +65 -0
- package/tests/integration/features.test.ts +922 -0
- package/tests/integration/full_stack.test.ts +262 -0
- package/tests/integration/setup.ts +45 -0
- package/tests/ir.test.ts +32 -0
- package/tests/namespace.test.ts +395 -0
- package/tests/permissions.test.ts +55 -0
- package/tests/pinia.test.ts +48 -0
- package/tests/realtime.test.ts +22 -0
- package/tests/runtime.test.ts +80 -0
- package/tests/subscribable_gen.test.ts +72 -0
- package/tests/subscribable_reactivity.test.ts +258 -0
- package/tests/venues_gen.test.ts +25 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/README.md +0 -90
- package/bin/cli.js +0 -727
- package/docs/compiler/ADVANCED_FILTERS.md +0 -183
- package/docs/compiler/CODING_STANDARDS.md +0 -415
- package/docs/compiler/COMPARISON.md +0 -673
- package/docs/compiler/QUICKSTART.md +0 -326
- package/docs/compiler/README.md +0 -134
- package/docs/examples/README.md +0 -38
- package/docs/examples/blog.sql +0 -160
- package/docs/examples/venue-detail-simple.sql +0 -8
- package/docs/examples/venue-detail-subscribable.sql +0 -45
- package/docs/for-ai/claude-guide.md +0 -1210
- package/docs/getting-started/quickstart.md +0 -125
- package/docs/getting-started/subscriptions-quick-start.md +0 -203
- package/docs/getting-started/tutorial.md +0 -1104
- package/docs/guides/atomic-updates.md +0 -299
- package/docs/guides/client-stores.md +0 -730
- package/docs/guides/composite-primary-keys.md +0 -158
- package/docs/guides/custom-functions.md +0 -362
- package/docs/guides/drop-semantics.md +0 -554
- package/docs/guides/field-defaults.md +0 -240
- package/docs/guides/interpreter-vs-compiler.md +0 -237
- package/docs/guides/many-to-many.md +0 -929
- package/docs/guides/subscriptions.md +0 -537
- package/docs/reference/api.md +0 -1373
- package/docs/reference/client.md +0 -224
- package/src/client/stores/index.js +0 -8
- package/src/client/stores/useAppStore.js +0 -285
- package/src/client/stores/useWsStore.js +0 -289
- package/src/client/ws.js +0 -762
- package/src/compiler/cli/compile-example.js +0 -33
- package/src/compiler/cli/compile-subscribable.js +0 -43
- package/src/compiler/cli/debug-compile.js +0 -44
- package/src/compiler/cli/debug-parse.js +0 -26
- package/src/compiler/cli/debug-path-parser.js +0 -18
- package/src/compiler/cli/debug-subscribable-parser.js +0 -21
- package/src/compiler/cli/index.js +0 -174
- package/src/compiler/codegen/auth-codegen.js +0 -153
- package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
- package/src/compiler/codegen/graph-rules-codegen.js +0 -450
- package/src/compiler/codegen/notification-codegen.js +0 -232
- package/src/compiler/codegen/operation-codegen.js +0 -1382
- package/src/compiler/codegen/permission-codegen.js +0 -318
- package/src/compiler/codegen/subscribable-codegen.js +0 -827
- package/src/compiler/compiler.js +0 -371
- package/src/compiler/index.js +0 -11
- package/src/compiler/parser/entity-parser.js +0 -440
- package/src/compiler/parser/path-parser.js +0 -290
- package/src/compiler/parser/subscribable-parser.js +0 -244
- package/src/database/dzql-core.sql +0 -161
- package/src/database/migrations/001_schema.sql +0 -60
- package/src/database/migrations/002_functions.sql +0 -890
- package/src/database/migrations/003_operations.sql +0 -1135
- package/src/database/migrations/004_search.sql +0 -581
- package/src/database/migrations/005_entities.sql +0 -730
- package/src/database/migrations/006_auth.sql +0 -94
- package/src/database/migrations/007_events.sql +0 -133
- package/src/database/migrations/008_hello.sql +0 -18
- package/src/database/migrations/008a_meta.sql +0 -172
- package/src/database/migrations/009_subscriptions.sql +0 -240
- package/src/database/migrations/010_atomic_updates.sql +0 -157
- package/src/database/migrations/010_fix_m2m_events.sql +0 -94
- package/src/index.js +0 -40
- package/src/server/api.js +0 -9
- package/src/server/db.js +0 -442
- package/src/server/index.js +0 -317
- package/src/server/logger.js +0 -259
- package/src/server/mcp.js +0 -594
- package/src/server/meta-route.js +0 -251
- package/src/server/namespace.js +0 -292
- package/src/server/subscriptions.js +0 -351
- package/src/server/ws.js +0 -573
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { spawn, spawnSync } from "bun";
|
|
3
|
+
import { V2TestDatabase } from "./setup.js";
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
5
|
+
import { resolve, dirname } from "path";
|
|
6
|
+
|
|
7
|
+
// Tests may run from repo root or packages/tzql directory
|
|
8
|
+
// Use import.meta to get the correct path relative to this file
|
|
9
|
+
const TEST_DIR = dirname(new URL(import.meta.url).pathname);
|
|
10
|
+
const PACKAGE_ROOT = resolve(TEST_DIR, "../..");
|
|
11
|
+
const DIST_ROOT = resolve(PACKAGE_ROOT, "dist");
|
|
12
|
+
|
|
13
|
+
// Compile the venues example before tests run
|
|
14
|
+
function compileVenuesExample() {
|
|
15
|
+
const examplePath = resolve(PACKAGE_ROOT, "examples/venues.js");
|
|
16
|
+
const compilerPath = resolve(PACKAGE_ROOT, "src/cli/index.ts");
|
|
17
|
+
|
|
18
|
+
console.log("[Test] Compiling venues example...");
|
|
19
|
+
const result = spawnSync({
|
|
20
|
+
cmd: ["bun", "run", compilerPath, "compile", examplePath, "-o", DIST_ROOT],
|
|
21
|
+
cwd: PACKAGE_ROOT,
|
|
22
|
+
env: process.env,
|
|
23
|
+
stdout: "pipe",
|
|
24
|
+
stderr: "pipe"
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (result.exitCode !== 0) {
|
|
28
|
+
console.error("[Test] Compile failed:", new TextDecoder().decode(result.stderr));
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
console.log("[Test] Compile output:", new TextDecoder().decode(result.stdout));
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Pre-process the generated client to fix the import path for testing
|
|
36
|
+
function patchGeneratedClient() {
|
|
37
|
+
const clientPath = resolve(DIST_ROOT, "client/ws.ts");
|
|
38
|
+
if (!existsSync(clientPath)) return;
|
|
39
|
+
|
|
40
|
+
const content = readFileSync(clientPath, "utf8");
|
|
41
|
+
if (content.includes("from 'tzql/client'")) {
|
|
42
|
+
const tzqlClientPath = resolve(PACKAGE_ROOT, "src/client/index.ts");
|
|
43
|
+
const patched = content.replace(
|
|
44
|
+
"from 'tzql/client'",
|
|
45
|
+
`from '${tzqlClientPath}'`
|
|
46
|
+
);
|
|
47
|
+
writeFileSync(clientPath, patched);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Compile and check if dist exists
|
|
52
|
+
const COMPILE_SUCCESS = compileVenuesExample();
|
|
53
|
+
const DIST_EXISTS = COMPILE_SUCCESS && existsSync(resolve(DIST_ROOT, "runtime/manifest.json"));
|
|
54
|
+
|
|
55
|
+
if (DIST_EXISTS) {
|
|
56
|
+
patchGeneratedClient();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe.skipIf(!DIST_EXISTS)("Full Stack V2 Integration (Runtime + Client + Pinia)", () => {
|
|
60
|
+
let db: V2TestDatabase;
|
|
61
|
+
let sql: any;
|
|
62
|
+
let serverProcess: any;
|
|
63
|
+
let useVenueDetailStore: any;
|
|
64
|
+
let ws: any;
|
|
65
|
+
|
|
66
|
+
beforeAll(async () => {
|
|
67
|
+
// Dynamic imports for optional dependencies
|
|
68
|
+
const { createPinia, setActivePinia } = await import("pinia");
|
|
69
|
+
const clientPath = resolve(DIST_ROOT, "client/ws.ts");
|
|
70
|
+
ws = (await import(clientPath)).ws;
|
|
71
|
+
|
|
72
|
+
// 1. Setup DB
|
|
73
|
+
db = new V2TestDatabase();
|
|
74
|
+
sql = await db.setup();
|
|
75
|
+
|
|
76
|
+
// Apply Schema (Core + Venues)
|
|
77
|
+
const fs = require('fs');
|
|
78
|
+
const path = require('path');
|
|
79
|
+
const distPath = resolve(DIST_ROOT, 'db/migrations');
|
|
80
|
+
const coreSql = fs.readFileSync(path.join(distPath, '000_core.sql'), 'utf8');
|
|
81
|
+
const schemaFiles = fs.readdirSync(distPath)
|
|
82
|
+
.filter((f: string) => f.includes('_schema.sql'))
|
|
83
|
+
.sort().reverse();
|
|
84
|
+
const subscribableFiles = fs.readdirSync(distPath)
|
|
85
|
+
.filter((f: string) => f.includes('_subscribables.sql'))
|
|
86
|
+
.sort().reverse();
|
|
87
|
+
console.log(`[Test] Loading schema file: ${schemaFiles[0]}`);
|
|
88
|
+
const entitySql = fs.readFileSync(path.join(distPath, schemaFiles[0]), 'utf8');
|
|
89
|
+
|
|
90
|
+
await db.applySQL(coreSql);
|
|
91
|
+
await db.applySQL(entitySql);
|
|
92
|
+
|
|
93
|
+
// Load subscribable SQL functions
|
|
94
|
+
if (subscribableFiles.length > 0) {
|
|
95
|
+
console.log(`[Test] Loading subscribables file: ${subscribableFiles[0]}`);
|
|
96
|
+
const subscribableSql = fs.readFileSync(path.join(distPath, subscribableFiles[0]), 'utf8');
|
|
97
|
+
await db.applySQL(subscribableSql);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 2. Start Runtime Server
|
|
101
|
+
const runtimePath = new URL("../../src/runtime/index.ts", import.meta.url).pathname;
|
|
102
|
+
const manifestPath = resolve(DIST_ROOT, 'runtime/manifest.json');
|
|
103
|
+
|
|
104
|
+
const testDbUrl = db.baseUrl.replace(/\/[^/]*$/, `/${db.dbName}`);
|
|
105
|
+
console.log("[Test] Server DATABASE_URL:", testDbUrl);
|
|
106
|
+
console.log("[Test] MANIFEST_PATH:", manifestPath);
|
|
107
|
+
serverProcess = spawn({
|
|
108
|
+
cmd: ["bun", "run", runtimePath],
|
|
109
|
+
env: {
|
|
110
|
+
...process.env,
|
|
111
|
+
PORT: "3001", // Test port
|
|
112
|
+
DATABASE_URL: testDbUrl,
|
|
113
|
+
MANIFEST_PATH: manifestPath,
|
|
114
|
+
JWT_SECRET: "test-secret"
|
|
115
|
+
},
|
|
116
|
+
stdout: "pipe",
|
|
117
|
+
stderr: "pipe"
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Pipe stderr to console for debugging
|
|
121
|
+
const stderrReader = serverProcess.stderr.getReader();
|
|
122
|
+
(async () => {
|
|
123
|
+
while (true) {
|
|
124
|
+
const { done, value } = await stderrReader.read();
|
|
125
|
+
if (done) break;
|
|
126
|
+
console.error("[Server ERR]", new TextDecoder().decode(value));
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
|
|
130
|
+
// Pipe stdout to console (keep reading in background)
|
|
131
|
+
let serverReady = false;
|
|
132
|
+
const serverReadyPromise = new Promise<void>((resolve) => {
|
|
133
|
+
const stdoutReader = serverProcess.stdout.getReader();
|
|
134
|
+
(async () => {
|
|
135
|
+
while (true) {
|
|
136
|
+
const { done, value } = await stdoutReader.read();
|
|
137
|
+
if (done) break;
|
|
138
|
+
const text = new TextDecoder().decode(value);
|
|
139
|
+
console.log("[Server]", text.trim());
|
|
140
|
+
if (text.includes("Server listening") && !serverReady) {
|
|
141
|
+
serverReady = true;
|
|
142
|
+
resolve();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
})();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Wait for server to be ready
|
|
149
|
+
await serverReadyPromise;
|
|
150
|
+
|
|
151
|
+
// 3. Setup Pinia
|
|
152
|
+
setActivePinia(createPinia());
|
|
153
|
+
|
|
154
|
+
// Import generated store
|
|
155
|
+
const storePath = resolve(DIST_ROOT, "client/stores/useVenueDetailStore.ts");
|
|
156
|
+
const mod = await import(storePath);
|
|
157
|
+
useVenueDetailStore = mod.useVenueDetailStore;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
afterAll(async () => {
|
|
161
|
+
if (serverProcess) serverProcess.kill();
|
|
162
|
+
await db.teardown();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("should receive connection:ready on connect (anonymous)", async () => {
|
|
166
|
+
// Connect Client without token
|
|
167
|
+
await ws.connect("ws://localhost:3001/ws");
|
|
168
|
+
|
|
169
|
+
// Wait briefly for connection:ready message
|
|
170
|
+
await new Promise(r => setTimeout(r, 100));
|
|
171
|
+
|
|
172
|
+
// Should be ready but no user
|
|
173
|
+
expect(ws.ready).toBe(true);
|
|
174
|
+
expect(ws.user).toBe(null);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("should register and login", async () => {
|
|
178
|
+
// Register
|
|
179
|
+
const reg = await ws.register({ email: "tester@example.com", password: "password123" });
|
|
180
|
+
expect(reg.user_id).toBeDefined();
|
|
181
|
+
|
|
182
|
+
// Login
|
|
183
|
+
const login = await ws.login({ email: "tester@example.com", password: "password123" });
|
|
184
|
+
expect(login.token).toBeDefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("should receive connection:ready with user profile on reconnect with token", async () => {
|
|
188
|
+
// Login to get a token
|
|
189
|
+
const login = await ws.login({ email: "tester@example.com", password: "password123" });
|
|
190
|
+
const token = login.token;
|
|
191
|
+
expect(token).toBeDefined();
|
|
192
|
+
|
|
193
|
+
// Close current connection
|
|
194
|
+
ws.ws?.close();
|
|
195
|
+
await new Promise(r => setTimeout(r, 100));
|
|
196
|
+
|
|
197
|
+
// Reconnect with token in URL (simulating what browser does with localStorage)
|
|
198
|
+
await ws.connect(`ws://localhost:3001/ws?token=${encodeURIComponent(token)}`);
|
|
199
|
+
|
|
200
|
+
// Wait for connection:ready message
|
|
201
|
+
await new Promise(r => setTimeout(r, 200));
|
|
202
|
+
|
|
203
|
+
// Should be ready with user profile
|
|
204
|
+
expect(ws.ready).toBe(true);
|
|
205
|
+
expect(ws.user).not.toBe(null);
|
|
206
|
+
expect(ws.user.email).toBe("tester@example.com");
|
|
207
|
+
// Password hash should be stripped
|
|
208
|
+
expect(ws.user.password_hash).toBeUndefined();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("should sync data via Pinia store", async () => {
|
|
212
|
+
const store = useVenueDetailStore();
|
|
213
|
+
|
|
214
|
+
// 1. Create Data (via SDK)
|
|
215
|
+
// We need an Org first because Venue requires it
|
|
216
|
+
const org = await ws.api.save_organisations({ name: "Test Org" });
|
|
217
|
+
|
|
218
|
+
// VERIFY GRAPH RULE: Check acts_for
|
|
219
|
+
const memberships = await sql`SELECT * FROM acts_for WHERE org_id = ${org.id}`;
|
|
220
|
+
console.log("[Test] Memberships:", memberships);
|
|
221
|
+
if (memberships.length === 0) {
|
|
222
|
+
console.error("[Test] Graph Rule Failed: No acts_for created!");
|
|
223
|
+
} else {
|
|
224
|
+
console.log("[Test] Active:", memberships[0].active);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Create Venue
|
|
228
|
+
const venue = await ws.api.save_venues({ name: "Live Sync Venue", org_id: org.id, address: "123 Web St" });
|
|
229
|
+
|
|
230
|
+
// 2. Bind Store (async - waits for first data)
|
|
231
|
+
const doc = await store.bind({ venue_id: venue.id });
|
|
232
|
+
|
|
233
|
+
// Vue reactivity auto-unwraps refs when accessed from a reactive object
|
|
234
|
+
// So doc.loading is already the value (boolean), not the ref
|
|
235
|
+
expect(doc.loading).toBe(false);
|
|
236
|
+
// Verify subscription received data with correct structure
|
|
237
|
+
expect(doc.data?.venues?.name).toBe("Live Sync Venue");
|
|
238
|
+
expect(doc.data?.venues?.org_id).toBe(org.id);
|
|
239
|
+
expect(doc.data?.org?.name).toBe("Test Org");
|
|
240
|
+
expect(doc.data?.sites).toEqual([]);
|
|
241
|
+
|
|
242
|
+
// 3. Update Data (via SDK) -> Should trigger Realtime Update
|
|
243
|
+
// Note: org_id needed for permission check on update
|
|
244
|
+
const updated = await ws.api.save_venues({ id: venue.id, org_id: org.id, name: "Updated via WebSocket" });
|
|
245
|
+
expect(updated.name).toBe("Updated via WebSocket");
|
|
246
|
+
|
|
247
|
+
// Check if events were written to the database
|
|
248
|
+
const events = await sql`SELECT * FROM dzql_v2.events ORDER BY id DESC LIMIT 5`;
|
|
249
|
+
console.log("[Test] Events in DB:", events.length, events.map((e: any) => ({ table: e.table_name, op: e.op, data_name: e.data?.name })));
|
|
250
|
+
|
|
251
|
+
// Manually trigger a NOTIFY to test if listener works
|
|
252
|
+
console.log("[Test] Manually triggering pg_notify...");
|
|
253
|
+
await sql`SELECT pg_notify('dzql_v2', '{"commit_id": 999}')`;
|
|
254
|
+
|
|
255
|
+
// Wait for event propagation
|
|
256
|
+
await new Promise(r => setTimeout(r, 500));
|
|
257
|
+
|
|
258
|
+
// 4. Verify Store Update via Realtime
|
|
259
|
+
console.log("[Test] After realtime wait - doc.data?.venues?.name:", doc.data?.venues?.name);
|
|
260
|
+
expect(doc.data?.venues?.name).toBe("Updated via WebSocket");
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import postgres from "postgres";
|
|
2
|
+
|
|
3
|
+
export class V2TestDatabase {
|
|
4
|
+
adminSql: any;
|
|
5
|
+
testSql: any;
|
|
6
|
+
dbName: string;
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
this.dbName = `tzql_test_${process.pid}`;
|
|
11
|
+
this.baseUrl = process.env.DATABASE_URL || "postgres://dzql_test:dzql_test@localhost:5433/dzql_test";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async setup() {
|
|
15
|
+
// 1. Connect as admin to create DB
|
|
16
|
+
const adminUrl = this.baseUrl; // Assumes baseUrl points to accessible DB
|
|
17
|
+
this.adminSql = postgres(adminUrl, { max: 1, onnotice: () => {} });
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await this.adminSql.unsafe(`DROP DATABASE IF EXISTS ${this.dbName}`);
|
|
21
|
+
await this.adminSql.unsafe(`CREATE DATABASE ${this.dbName}`);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
// Ignore if DB creation fails (might exist)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 2. Connect to the fresh Test DB
|
|
27
|
+
const testUrl = this.baseUrl.replace(/\/[^/]*$/, `/${this.dbName}`);
|
|
28
|
+
this.testSql = postgres(testUrl, { onnotice: () => {} });
|
|
29
|
+
|
|
30
|
+
return this.testSql;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async applySQL(sqlContent: string) {
|
|
34
|
+
if (!this.testSql) throw new Error("DB not connected");
|
|
35
|
+
await this.testSql.unsafe(sqlContent);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async teardown() {
|
|
39
|
+
if (this.testSql) await this.testSql.end();
|
|
40
|
+
if (this.adminSql) {
|
|
41
|
+
await this.adminSql.unsafe(`DROP DATABASE IF EXISTS ${this.dbName}`);
|
|
42
|
+
await this.adminSql.end();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
package/tests/ir.test.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { generateIR } from "../src/cli/compiler/ir.js";
|
|
3
|
+
|
|
4
|
+
const rawDomain = {
|
|
5
|
+
entities: {
|
|
6
|
+
users: {
|
|
7
|
+
schema: {
|
|
8
|
+
id: "serial PRIMARY KEY",
|
|
9
|
+
name: "text NOT NULL"
|
|
10
|
+
},
|
|
11
|
+
permissions: {
|
|
12
|
+
view: ["public"]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
subscribables: {}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("IR Generator", () => {
|
|
20
|
+
test("should normalize entities", () => {
|
|
21
|
+
const ir = generateIR(rawDomain);
|
|
22
|
+
|
|
23
|
+
expect(ir.entities.users).toBeDefined();
|
|
24
|
+
expect(ir.entities.users.primaryKey).toEqual(["id"]);
|
|
25
|
+
expect(ir.entities.users.columns).toHaveLength(2);
|
|
26
|
+
expect(ir.entities.users.columns[0].name).toBe("id");
|
|
27
|
+
|
|
28
|
+
// Check permission normalization
|
|
29
|
+
expect(ir.entities.users.permissions.view).toEqual(["public"]);
|
|
30
|
+
expect(ir.entities.users.permissions.create).toEqual([]); // Defaulted
|
|
31
|
+
});
|
|
32
|
+
});
|