dzql 0.5.33 → 0.6.0

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.
Files changed (150) hide show
  1. package/.env.sample +28 -0
  2. package/compose.yml +28 -0
  3. package/dist/client/index.ts +1 -0
  4. package/dist/client/stores/useMyProfileStore.ts +114 -0
  5. package/dist/client/stores/useOrgDashboardStore.ts +131 -0
  6. package/dist/client/stores/useVenueDetailStore.ts +117 -0
  7. package/dist/client/ws.ts +716 -0
  8. package/dist/db/migrations/000_core.sql +92 -0
  9. package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
  10. package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
  11. package/dist/runtime/manifest.json +1562 -0
  12. package/docs/README.md +293 -36
  13. package/docs/feature-requests/applyPatch-bug-report.md +85 -0
  14. package/docs/feature-requests/connection-ready-profile.md +57 -0
  15. package/docs/feature-requests/hidden-bug-report.md +111 -0
  16. package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
  17. package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
  18. package/docs/feature-requests/todo.md +146 -0
  19. package/docs/for_ai.md +641 -0
  20. package/docs/project-setup.md +432 -0
  21. package/examples/blog.ts +50 -0
  22. package/examples/invalid.ts +18 -0
  23. package/examples/venues.js +485 -0
  24. package/package.json +23 -60
  25. package/src/cli/codegen/client.ts +99 -0
  26. package/src/cli/codegen/manifest.ts +95 -0
  27. package/src/cli/codegen/pinia.ts +174 -0
  28. package/src/cli/codegen/realtime.ts +58 -0
  29. package/src/cli/codegen/sql.ts +698 -0
  30. package/src/cli/codegen/subscribable_sql.ts +547 -0
  31. package/src/cli/codegen/subscribable_store.ts +184 -0
  32. package/src/cli/codegen/types.ts +142 -0
  33. package/src/cli/compiler/analyzer.ts +52 -0
  34. package/src/cli/compiler/graph_rules.ts +251 -0
  35. package/src/cli/compiler/ir.ts +233 -0
  36. package/src/cli/compiler/loader.ts +132 -0
  37. package/src/cli/compiler/permissions.ts +227 -0
  38. package/src/cli/index.ts +164 -0
  39. package/src/client/index.ts +1 -0
  40. package/src/client/ws.ts +286 -0
  41. package/src/create/.env.example +8 -0
  42. package/src/create/README.md +101 -0
  43. package/src/create/compose.yml +14 -0
  44. package/src/create/domain.ts +153 -0
  45. package/src/create/package.json +24 -0
  46. package/src/create/server.ts +18 -0
  47. package/src/create/setup.sh +11 -0
  48. package/src/create/tsconfig.json +15 -0
  49. package/src/runtime/auth.ts +39 -0
  50. package/src/runtime/db.ts +33 -0
  51. package/src/runtime/errors.ts +51 -0
  52. package/src/runtime/index.ts +98 -0
  53. package/src/runtime/js_functions.ts +63 -0
  54. package/src/runtime/manifest_loader.ts +29 -0
  55. package/src/runtime/namespace.ts +483 -0
  56. package/src/runtime/server.ts +87 -0
  57. package/src/runtime/ws.ts +197 -0
  58. package/src/shared/ir.ts +197 -0
  59. package/tests/client.test.ts +38 -0
  60. package/tests/codegen.test.ts +71 -0
  61. package/tests/compiler.test.ts +45 -0
  62. package/tests/graph_rules.test.ts +173 -0
  63. package/tests/integration/db.test.ts +174 -0
  64. package/tests/integration/e2e.test.ts +65 -0
  65. package/tests/integration/features.test.ts +922 -0
  66. package/tests/integration/full_stack.test.ts +262 -0
  67. package/tests/integration/setup.ts +45 -0
  68. package/tests/ir.test.ts +32 -0
  69. package/tests/namespace.test.ts +395 -0
  70. package/tests/permissions.test.ts +55 -0
  71. package/tests/pinia.test.ts +48 -0
  72. package/tests/realtime.test.ts +22 -0
  73. package/tests/runtime.test.ts +80 -0
  74. package/tests/subscribable_gen.test.ts +72 -0
  75. package/tests/subscribable_reactivity.test.ts +258 -0
  76. package/tests/venues_gen.test.ts +25 -0
  77. package/tsconfig.json +20 -0
  78. package/tsconfig.tsbuildinfo +1 -0
  79. package/README.md +0 -90
  80. package/bin/cli.js +0 -727
  81. package/docs/compiler/ADVANCED_FILTERS.md +0 -183
  82. package/docs/compiler/CODING_STANDARDS.md +0 -415
  83. package/docs/compiler/COMPARISON.md +0 -673
  84. package/docs/compiler/QUICKSTART.md +0 -326
  85. package/docs/compiler/README.md +0 -134
  86. package/docs/examples/README.md +0 -38
  87. package/docs/examples/blog.sql +0 -160
  88. package/docs/examples/venue-detail-simple.sql +0 -8
  89. package/docs/examples/venue-detail-subscribable.sql +0 -45
  90. package/docs/for-ai/claude-guide.md +0 -1210
  91. package/docs/getting-started/quickstart.md +0 -125
  92. package/docs/getting-started/subscriptions-quick-start.md +0 -203
  93. package/docs/getting-started/tutorial.md +0 -1104
  94. package/docs/guides/atomic-updates.md +0 -299
  95. package/docs/guides/client-stores.md +0 -730
  96. package/docs/guides/composite-primary-keys.md +0 -158
  97. package/docs/guides/custom-functions.md +0 -362
  98. package/docs/guides/drop-semantics.md +0 -554
  99. package/docs/guides/field-defaults.md +0 -240
  100. package/docs/guides/interpreter-vs-compiler.md +0 -237
  101. package/docs/guides/many-to-many.md +0 -929
  102. package/docs/guides/subscriptions.md +0 -537
  103. package/docs/reference/api.md +0 -1373
  104. package/docs/reference/client.md +0 -224
  105. package/src/client/stores/index.js +0 -8
  106. package/src/client/stores/useAppStore.js +0 -285
  107. package/src/client/stores/useWsStore.js +0 -289
  108. package/src/client/ws.js +0 -762
  109. package/src/compiler/cli/compile-example.js +0 -33
  110. package/src/compiler/cli/compile-subscribable.js +0 -43
  111. package/src/compiler/cli/debug-compile.js +0 -44
  112. package/src/compiler/cli/debug-parse.js +0 -26
  113. package/src/compiler/cli/debug-path-parser.js +0 -18
  114. package/src/compiler/cli/debug-subscribable-parser.js +0 -21
  115. package/src/compiler/cli/index.js +0 -174
  116. package/src/compiler/codegen/auth-codegen.js +0 -153
  117. package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
  118. package/src/compiler/codegen/graph-rules-codegen.js +0 -450
  119. package/src/compiler/codegen/notification-codegen.js +0 -232
  120. package/src/compiler/codegen/operation-codegen.js +0 -1382
  121. package/src/compiler/codegen/permission-codegen.js +0 -318
  122. package/src/compiler/codegen/subscribable-codegen.js +0 -827
  123. package/src/compiler/compiler.js +0 -371
  124. package/src/compiler/index.js +0 -11
  125. package/src/compiler/parser/entity-parser.js +0 -440
  126. package/src/compiler/parser/path-parser.js +0 -290
  127. package/src/compiler/parser/subscribable-parser.js +0 -244
  128. package/src/database/dzql-core.sql +0 -161
  129. package/src/database/migrations/001_schema.sql +0 -60
  130. package/src/database/migrations/002_functions.sql +0 -890
  131. package/src/database/migrations/003_operations.sql +0 -1135
  132. package/src/database/migrations/004_search.sql +0 -581
  133. package/src/database/migrations/005_entities.sql +0 -730
  134. package/src/database/migrations/006_auth.sql +0 -94
  135. package/src/database/migrations/007_events.sql +0 -133
  136. package/src/database/migrations/008_hello.sql +0 -18
  137. package/src/database/migrations/008a_meta.sql +0 -172
  138. package/src/database/migrations/009_subscriptions.sql +0 -240
  139. package/src/database/migrations/010_atomic_updates.sql +0 -157
  140. package/src/database/migrations/010_fix_m2m_events.sql +0 -94
  141. package/src/index.js +0 -40
  142. package/src/server/api.js +0 -9
  143. package/src/server/db.js +0 -442
  144. package/src/server/index.js +0 -317
  145. package/src/server/logger.js +0 -259
  146. package/src/server/mcp.js +0 -594
  147. package/src/server/meta-route.js +0 -251
  148. package/src/server/namespace.js +0 -292
  149. package/src/server/subscriptions.js +0 -351
  150. package/src/server/ws.js +0 -573
@@ -0,0 +1,197 @@
1
+ import { ServerWebSocket } from "bun";
2
+ import { handleRequest } from "./server.js"; // The secure router
3
+ import { verifyToken, signToken } from "./auth.js";
4
+ import { Database } from "./db.js";
5
+
6
+ interface WSContext {
7
+ id: string;
8
+ userId?: number;
9
+ subscriptions: Set<string>; // Set of subscription IDs
10
+ lastPing: number;
11
+ token?: string; // Token passed from URL query params during upgrade
12
+ }
13
+
14
+ export class WebSocketServer {
15
+ private connections = new Map<string, ServerWebSocket<WSContext>>();
16
+ private db: Database;
17
+
18
+ constructor(db: Database) {
19
+ this.db = db;
20
+ // Start heartbeat interval
21
+ setInterval(() => this.heartbeat(), 30000);
22
+ }
23
+
24
+ // Bun.serve websocket handler hooks
25
+ get handlers() {
26
+ return {
27
+ open: (ws: ServerWebSocket<WSContext>) => this.handleOpen(ws),
28
+ message: (ws: ServerWebSocket<WSContext>, message: string) => this.handleMessage(ws, message),
29
+ close: (ws: ServerWebSocket<WSContext>) => this.handleClose(ws),
30
+ drain: (ws: ServerWebSocket<WSContext>) => {}
31
+ };
32
+ }
33
+
34
+ private async handleOpen(ws: ServerWebSocket<WSContext>) {
35
+ const id = Math.random().toString(36).slice(2);
36
+ const token = ws.data?.token;
37
+
38
+ ws.data = {
39
+ id,
40
+ subscriptions: new Set(),
41
+ lastPing: Date.now()
42
+ };
43
+ this.connections.set(id, ws);
44
+ console.log(`[WS] Client ${id} connected`);
45
+
46
+ // Subscribe to global broadcast channel initially
47
+ ws.subscribe("broadcast");
48
+
49
+ // If token was provided in URL, verify and authenticate
50
+ let user: any = null;
51
+ if (token) {
52
+ try {
53
+ const session = await verifyToken(token);
54
+ ws.data.userId = session.userId;
55
+ console.log(`[WS] Client ${id} authenticated via token as user ${session.userId}`);
56
+
57
+ // Fetch user profile using get_users
58
+ try {
59
+ const profile = await handleRequest(this.db, 'get_users', { id: session.userId }, session.userId);
60
+ if (profile) {
61
+ // Remove sensitive data
62
+ const { password_hash, ...safeProfile } = profile;
63
+ user = safeProfile;
64
+ }
65
+ } catch (e) {
66
+ console.error(`[WS] Failed to fetch profile for user ${session.userId}:`, e);
67
+ // Still authenticated, just no profile
68
+ user = { id: session.userId };
69
+ }
70
+ } catch (e: any) {
71
+ console.log(`[WS] Client ${id} token verification failed:`, e.message);
72
+ // Token invalid, user remains null (anonymous connection)
73
+ }
74
+ }
75
+
76
+ // Send connection:ready message
77
+ ws.send(JSON.stringify({
78
+ jsonrpc: "2.0",
79
+ method: "connection:ready",
80
+ params: { user }
81
+ }));
82
+ }
83
+
84
+ private async handleMessage(ws: ServerWebSocket<WSContext>, message: string) {
85
+ ws.data.lastPing = Date.now(); // Update activity
86
+
87
+ try {
88
+ const req = JSON.parse(message);
89
+
90
+ // Handle Ping (Client-side heartbeat)
91
+ if (req.method === 'ping') {
92
+ ws.send(JSON.stringify({ id: req.id, result: 'pong' }));
93
+ return;
94
+ }
95
+
96
+ // Handle Auth Handshake
97
+ if (req.method === 'auth') {
98
+ try {
99
+ const session = await verifyToken(req.params.token);
100
+ ws.data.userId = session.userId;
101
+ ws.send(JSON.stringify({ id: req.id, result: { success: true, userId: session.userId } }));
102
+ console.log(`[WS] Client ${ws.data.id} authenticated as user ${session.userId}`);
103
+ } catch (e: any) {
104
+ ws.send(JSON.stringify({ id: req.id, error: { code: 'UNAUTHORIZED', message: e.message } }));
105
+ }
106
+ return;
107
+ }
108
+
109
+ // Require Auth for other methods?
110
+ // V2 spec says `p_user_id` is passed to functions.
111
+ // If not auth'd, we can pass null or throw.
112
+ const userId = ws.data.userId || null;
113
+
114
+ // Handle RPC
115
+ if (req.method) {
116
+ if (req.method.startsWith("subscribe_")) {
117
+ // Handle Subscription Registration
118
+ const subscribableName = req.method.replace("subscribe_", "");
119
+ const getFnName = `get_${subscribableName}`;
120
+
121
+ try {
122
+ // Call the get_ function to fetch initial data
123
+ const snapshot = await handleRequest(this.db, getFnName, req.params, userId);
124
+
125
+ // Register subscription
126
+ const subId = `${subscribableName}:${JSON.stringify(req.params)}`;
127
+ ws.data.subscriptions.add(subId);
128
+
129
+ // Return snapshot with subscription_id and schema
130
+ ws.send(JSON.stringify({
131
+ id: req.id,
132
+ result: {
133
+ subscription_id: subId,
134
+ data: snapshot.data,
135
+ schema: snapshot.schema
136
+ }
137
+ }));
138
+ } catch (e: any) {
139
+ ws.send(JSON.stringify({
140
+ id: req.id,
141
+ error: { code: e.code || 'INTERNAL_ERROR', message: e.message }
142
+ }));
143
+ }
144
+ } else {
145
+ // Normal Function Call
146
+ const result = await handleRequest(this.db, req.method, req.params, userId);
147
+
148
+ // Auto-generate token for auth methods
149
+ if (req.method === 'login_user' || req.method === 'register_user') {
150
+ const token = await signToken({ user_id: result.user_id, role: 'user' });
151
+ // Return profile + token
152
+ ws.send(JSON.stringify({ id: req.id, result: { ...result, token } }));
153
+ } else {
154
+ ws.send(JSON.stringify({ id: req.id, result }));
155
+ }
156
+ }
157
+ }
158
+
159
+ } catch (e: any) {
160
+ console.error(`[WS] Error processing message from ${ws.data.id}:`, e);
161
+ // Try to send error back if JSON parse didn't fail
162
+ try {
163
+ const reqId = JSON.parse(message).id;
164
+ ws.send(JSON.stringify({ id: reqId, error: { code: 'INTERNAL_ERROR', message: e.message } }));
165
+ } catch (ignore) {}
166
+ }
167
+ }
168
+
169
+ private handleClose(ws: ServerWebSocket<WSContext>) {
170
+ this.connections.delete(ws.data.id);
171
+ console.log(`[WS] Client ${ws.data.id} disconnected`);
172
+ }
173
+
174
+ private heartbeat() {
175
+ const now = Date.now();
176
+ for (const [id, ws] of this.connections) {
177
+ if (now - ws.data.lastPing > 60000) {
178
+ console.log(`[WS] Client ${id} timed out`);
179
+ ws.close();
180
+ this.connections.delete(id);
181
+ }
182
+ }
183
+ }
184
+
185
+ public broadcast(message: string) {
186
+ // Use Bun's native publish for efficiency
187
+ // 'broadcast' topic is subscribed by all on connect
188
+ // In V2, we might have specific topics per subscription key
189
+ // server.publish("broadcast", message);
190
+ // Since this class doesn't hold the 'server' instance directly,
191
+ // we iterate or need to pass server in.
192
+ // For now, iteration is fine for prototype.
193
+ for (const ws of this.connections.values()) {
194
+ ws.send(message);
195
+ }
196
+ }
197
+ }
@@ -0,0 +1,197 @@
1
+ // ============================================
2
+ // INPUT TYPES - Used for domain configuration
3
+ // ============================================
4
+
5
+ /** Action types supported in graph rules */
6
+ export type GraphRuleActionType = 'create' | 'update' | 'delete' | 'validate' | 'execute' | 'reactor';
7
+
8
+ /** Configuration for a graph rule action */
9
+ export interface GraphRuleActionConfig {
10
+ type: GraphRuleActionType;
11
+ entity?: string; // Target entity for create action
12
+ target?: string; // Target entity for update/delete actions
13
+ name?: string; // Reactor name for reactor type
14
+ function?: string; // Function name for validate/execute
15
+ data?: Record<string, string>; // Data for create/update (field -> @variable)
16
+ match?: Record<string, string>; // Match condition for update/delete
17
+ params?: Record<string, string>; // Parameters for reactor/validate/execute
18
+ error_message?: string; // Error message for validate
19
+ }
20
+
21
+ /** Configuration for a named graph rule */
22
+ export interface GraphRuleConfig {
23
+ description?: string;
24
+ condition?: string; // e.g., "@before.status = 'draft' AND @after.status = 'posted'"
25
+ actions: GraphRuleActionConfig[];
26
+ }
27
+
28
+ /** Include configuration - either a simple string (entity name) or full object */
29
+ export interface IncludeConfig {
30
+ entity: string;
31
+ filter?: Record<string, string>;
32
+ includes?: Record<string, string | IncludeConfig>;
33
+ }
34
+
35
+ /** Many-to-many relationship configuration */
36
+ export interface ManyToManyConfig {
37
+ junctionTable: string;
38
+ localKey: string;
39
+ foreignKey: string;
40
+ targetEntity: string;
41
+ idField: string;
42
+ expand?: boolean;
43
+ }
44
+
45
+ /** Permission paths configuration */
46
+ export interface PermissionPathsConfig {
47
+ view?: string[];
48
+ create?: string[];
49
+ update?: string[];
50
+ delete?: string[];
51
+ }
52
+
53
+ /** Temporal fields configuration */
54
+ export interface TemporalConfig {
55
+ validFrom: string;
56
+ validTo: string;
57
+ }
58
+
59
+ /** Graph rules grouped by trigger */
60
+ export interface GraphRulesConfig {
61
+ on_create?: Record<string, GraphRuleConfig>;
62
+ on_update?: Record<string, GraphRuleConfig>;
63
+ on_delete?: Record<string, GraphRuleConfig>;
64
+ primary_key?: string[]; // Composite primary key fields
65
+ many_to_many?: Record<string, ManyToManyConfig>;
66
+ }
67
+
68
+ /** Entity configuration as provided in domain file */
69
+ export interface EntityConfig {
70
+ schema: Record<string, string>;
71
+ label?: string;
72
+ searchable?: string[];
73
+ hidden?: string[];
74
+ primaryKey?: string[];
75
+ managed?: boolean;
76
+ softDelete?: boolean;
77
+ fieldDefaults?: Record<string, string>;
78
+ permissions?: PermissionPathsConfig;
79
+ includes?: Record<string, string | IncludeConfig>;
80
+ manyToMany?: Record<string, ManyToManyConfig>;
81
+ graphRules?: GraphRulesConfig;
82
+ temporal?: TemporalConfig;
83
+ constraints?: string[];
84
+ indexes?: string[];
85
+ notifications?: Record<string, string[]>;
86
+ }
87
+
88
+ /** Subscribable configuration as provided in domain file */
89
+ export interface SubscribableConfig {
90
+ params: Record<string, string>;
91
+ root: { entity: string; key: string };
92
+ includes?: Record<string, string | IncludeConfig>;
93
+ scopeTables?: string[];
94
+ canSubscribe?: string[];
95
+ }
96
+
97
+ /** Custom function configuration */
98
+ export interface CustomFunctionConfig {
99
+ name: string;
100
+ sql: string;
101
+ args?: string[];
102
+ }
103
+
104
+ /** Complete domain configuration as provided in domain file */
105
+ export interface DomainConfig {
106
+ entities: Record<string, EntityConfig>;
107
+ subscribables?: Record<string, SubscribableConfig>;
108
+ customFunctions?: CustomFunctionConfig[];
109
+ }
110
+
111
+ // ============================================
112
+ // IR TYPES - Intermediate representation
113
+ // ============================================
114
+
115
+ export interface EntityIR {
116
+ name: string;
117
+ table: string;
118
+ primaryKey: string[];
119
+ columns: Array<{ name: string; type: string; isArray: boolean }>;
120
+ labelField?: string;
121
+ softDelete?: boolean;
122
+ managed?: boolean; // If false, skip CRUD function generation (for junction tables)
123
+ hidden?: string[]; // Fields to exclude from query results (e.g., password_hash)
124
+ fieldDefaults?: Record<string, string>;
125
+ permissions: {
126
+ view: string[];
127
+ create: string[];
128
+ update: string[];
129
+ delete: string[];
130
+ };
131
+ relationships: Record<string, RelationshipIR>;
132
+ manyToMany: Record<string, ManyToManyIR>;
133
+ graphRules: {
134
+ onCreate: GraphRuleIR[];
135
+ onUpdate: GraphRuleIR[];
136
+ onDelete: GraphRuleIR[];
137
+ };
138
+ }
139
+
140
+ export interface ManyToManyIR {
141
+ junctionTable: string;
142
+ localKey: string;
143
+ foreignKey: string;
144
+ targetEntity: string;
145
+ idField: string;
146
+ expand: boolean;
147
+ }
148
+
149
+ export interface RelationshipIR {
150
+ type: 'one_to_many' | 'many_to_one' | 'many_to_many';
151
+ targetEntity: string;
152
+ localKey: string;
153
+ foreignKey: string;
154
+ }
155
+
156
+ export interface GraphRuleIR {
157
+ trigger: 'create' | 'update' | 'delete';
158
+ action: 'create' | 'update' | 'delete' | 'reactor' | 'validate' | 'execute';
159
+ target: string; // entity name, reactor name, or function name
160
+ ruleName?: string; // The name of the rule (for comments)
161
+ description?: string; // Human-readable description
162
+ condition?: string; // e.g., "@before.status = 'draft' AND @after.status = 'posted'"
163
+ params: Record<string, string>; // Data for create, or params for reactor/validate/execute
164
+ match?: Record<string, string>; // WHERE clause for update/delete actions
165
+ error_message?: string; // Error message for validate action
166
+ }
167
+
168
+ export interface SubscribableIR {
169
+ name: string;
170
+ params: Record<string, string>;
171
+ root: {
172
+ entity: string;
173
+ key: string;
174
+ };
175
+ includes: Record<string, IncludeIR>;
176
+ scopeTables: string[];
177
+ canSubscribe: string[]; // Permission paths
178
+ }
179
+
180
+ export interface IncludeIR {
181
+ relation: string; // The key (e.g., 'sites')
182
+ entity: string; // The target table (e.g., 'sites')
183
+ filter?: Record<string, string>;
184
+ includes?: Record<string, IncludeIR>; // Nested includes
185
+ }
186
+
187
+ export interface CustomFunctionIR {
188
+ name: string;
189
+ sql: string;
190
+ args?: string[]; // For manifest allowlist
191
+ }
192
+
193
+ export interface DomainIR {
194
+ entities: Record<string, EntityIR>;
195
+ subscribables: Record<string, SubscribableIR>;
196
+ customFunctions: CustomFunctionIR[];
197
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { generateClientSDK } from "../src/cli/codegen/client.js";
3
+ import { generateManifest } from "../src/cli/codegen/manifest.js";
4
+ import { generateIR } from "../src/cli/compiler/ir.js";
5
+
6
+ const mockManifest = generateManifest(generateIR({
7
+ entities: {
8
+ posts: {
9
+ schema: { id: "serial primary key", title: "text" },
10
+ permissions: { create: [], view: [] }
11
+ }
12
+ },
13
+ subscribables: {}
14
+ }));
15
+
16
+ describe("Client SDK Generation", () => {
17
+ test("should generate a TypeScript SDK with typed API", () => {
18
+ const tsCode = generateClientSDK(mockManifest);
19
+
20
+ // Check imports
21
+ expect(tsCode).toContain("import { WebSocketManager } from 'tzql/client'");
22
+
23
+ // Check interface definition
24
+ expect(tsCode).toContain("export interface TzqlAPI {");
25
+ expect(tsCode).toContain("save_posts: (params: SavePostsParams) => Promise<Posts>");
26
+ expect(tsCode).toContain("get_posts: (params: PostsPK) => Promise<Posts | null>");
27
+
28
+ // Check class definition
29
+ expect(tsCode).toContain("export class GeneratedWebSocketManager extends WebSocketManager");
30
+ expect(tsCode).toContain("api: TzqlAPI");
31
+
32
+ // Check API implementation
33
+ expect(tsCode).toContain("this.call('save_posts', params)");
34
+
35
+ // Check singleton export
36
+ expect(tsCode).toContain("export const ws = new GeneratedWebSocketManager()");
37
+ });
38
+ });
@@ -0,0 +1,71 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { generateCoreSQL, generateEntitySQL } from "../src/cli/codegen/sql.js";
3
+ import { generateManifest } from "../src/cli/codegen/manifest.js";
4
+ import { generateIR } from "../src/cli/compiler/ir.js";
5
+
6
+ const mockEntityIR = {
7
+ name: "posts",
8
+ table: "posts",
9
+ primaryKey: ["id"],
10
+ columns: [
11
+ { name: "id", type: "serial PRIMARY KEY" },
12
+ { name: "title", type: "text NOT NULL" }
13
+ ],
14
+ permissions: {
15
+ create: [],
16
+ view: []
17
+ },
18
+ graphRules: {
19
+ onCreate: []
20
+ }
21
+ };
22
+
23
+ describe("SQL Code Generation", () => {
24
+ test("generateCoreSQL should produce migration table", () => {
25
+ const sql = generateCoreSQL();
26
+ expect(sql).toContain("CREATE SCHEMA IF NOT EXISTS dzql_v2");
27
+ expect(sql).toContain("CREATE TABLE IF NOT EXISTS dzql_v2.migrations");
28
+ });
29
+
30
+ test("generateEntitySQL should produce save function", () => {
31
+ const sql = generateEntitySQL("posts", mockEntityIR);
32
+ expect(sql).toContain("CREATE OR REPLACE FUNCTION dzql_v2.save_posts");
33
+ expect(sql).toContain("AND EXISTS(SELECT 1 FROM posts WHERE"); // Check existence check (composite PK support)
34
+ expect(sql).toContain("UPDATE posts SET"); // Check update branch
35
+ expect(sql).toContain("INSERT INTO posts"); // Check insert branch
36
+ });
37
+ });
38
+
39
+ const mockRawEntity = {
40
+ schema: {
41
+ id: "serial PRIMARY KEY",
42
+ title: "text NOT NULL"
43
+ },
44
+ permissions: {
45
+ create: [],
46
+ view: []
47
+ }
48
+ };
49
+
50
+ describe("Manifest Generation", () => {
51
+ test("should generate allowlist", () => {
52
+ // Create IR first
53
+ const ir = generateIR({
54
+ entities: { posts: mockRawEntity },
55
+ subscribables: {}
56
+ });
57
+
58
+ const manifest = generateManifest(ir);
59
+
60
+ expect(manifest.version).toBe("2.0.0");
61
+ expect(manifest.functions).toBeDefined();
62
+
63
+ // Check allowlist
64
+ expect(manifest.functions["save_posts"]).toBeDefined();
65
+ expect(manifest.functions["get_posts"]).toBeDefined();
66
+ expect(manifest.functions["delete_posts"]).toBeDefined();
67
+
68
+ // Check signatures (basic check for now)
69
+ expect(manifest.functions["save_posts"].args).toEqual(["p_user_id", "p_data"]);
70
+ });
71
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { analyzeDomain } from "../src/cli/compiler/analyzer.js";
3
+
4
+ // Mock domain for testing
5
+ const validDomain = {
6
+ entities: {
7
+ users: { schema: { id: "serial primary key" } },
8
+ posts: { schema: { id: "serial primary key", user_id: "int" } }
9
+ },
10
+ subscribables: {
11
+ user_posts: {
12
+ root: { entity: "users" },
13
+ includes: {
14
+ posts: { entity: "posts" }
15
+ }
16
+ }
17
+ }
18
+ };
19
+
20
+ const invalidDomain = {
21
+ entities: {
22
+ users: { schema: { id: "serial primary key" } }
23
+ },
24
+ subscribables: {
25
+ broken_feed: {
26
+ root: { entity: "users" },
27
+ includes: {
28
+ posts: { entity: "missing_posts" } // <--- Reference to missing entity
29
+ }
30
+ }
31
+ }
32
+ };
33
+
34
+ describe("Compiler Analyzer", () => {
35
+ test("should pass for valid domain", () => {
36
+ const errors = analyzeDomain(validDomain);
37
+ expect(errors).toHaveLength(0);
38
+ });
39
+
40
+ test("should fail for missing entity reference", () => {
41
+ const errors = analyzeDomain(invalidDomain);
42
+ expect(errors).toHaveLength(1);
43
+ expect(errors[0]).toContain("references unknown entity 'missing_posts'");
44
+ });
45
+ });