dzql 0.6.3 → 0.6.6

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 (41) hide show
  1. package/README.md +33 -0
  2. package/docs/for_ai.md +14 -18
  3. package/docs/project-setup.md +15 -14
  4. package/package.json +28 -6
  5. package/src/cli/codegen/client.ts +5 -6
  6. package/src/cli/codegen/subscribable_store.ts +5 -5
  7. package/src/runtime/ws.ts +16 -15
  8. package/.env.sample +0 -28
  9. package/compose.yml +0 -28
  10. package/dist/client/index.ts +0 -1
  11. package/dist/client/stores/useMyProfileStore.ts +0 -114
  12. package/dist/client/stores/useOrgDashboardStore.ts +0 -131
  13. package/dist/client/stores/useVenueDetailStore.ts +0 -117
  14. package/dist/client/ws.ts +0 -716
  15. package/dist/db/migrations/000_core.sql +0 -92
  16. package/dist/db/migrations/20260101T235039268Z_schema.sql +0 -3020
  17. package/dist/db/migrations/20260101T235039268Z_subscribables.sql +0 -371
  18. package/dist/runtime/manifest.json +0 -1562
  19. package/examples/blog.ts +0 -50
  20. package/examples/invalid.ts +0 -18
  21. package/examples/venues.js +0 -485
  22. package/tests/client.test.ts +0 -38
  23. package/tests/codegen.test.ts +0 -71
  24. package/tests/compiler.test.ts +0 -45
  25. package/tests/graph_rules.test.ts +0 -173
  26. package/tests/integration/db.test.ts +0 -174
  27. package/tests/integration/e2e.test.ts +0 -65
  28. package/tests/integration/features.test.ts +0 -922
  29. package/tests/integration/full_stack.test.ts +0 -262
  30. package/tests/integration/setup.ts +0 -45
  31. package/tests/ir.test.ts +0 -32
  32. package/tests/namespace.test.ts +0 -395
  33. package/tests/permissions.test.ts +0 -55
  34. package/tests/pinia.test.ts +0 -48
  35. package/tests/realtime.test.ts +0 -22
  36. package/tests/runtime.test.ts +0 -80
  37. package/tests/subscribable_gen.test.ts +0 -72
  38. package/tests/subscribable_reactivity.test.ts +0 -258
  39. package/tests/venues_gen.test.ts +0 -25
  40. package/tsconfig.json +0 -20
  41. package/tsconfig.tsbuildinfo +0 -1
@@ -1,45 +0,0 @@
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
- });
@@ -1,173 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { compileGraphRules } from "../src/cli/compiler/graph_rules.js";
3
- import type { GraphRuleIR } from "../src/shared/ir.js";
4
-
5
- // Mock entity graph rules in IR format
6
- const mockGraphRules = {
7
- onCreate: [
8
- {
9
- trigger: 'create' as const,
10
- action: 'reactor' as const,
11
- target: 'notify_subscribers',
12
- params: { post_id: '@id' }
13
- }
14
- ] as GraphRuleIR[],
15
- onDelete: [
16
- {
17
- trigger: 'delete' as const,
18
- action: 'delete' as const,
19
- target: 'comments',
20
- params: { post_id: '@id' }
21
- }
22
- ] as GraphRuleIR[]
23
- };
24
-
25
- describe("Graph Rules Compiler", () => {
26
-
27
- test("should compile reactor to event insertion", () => {
28
- // Should insert into dzql_v2.events with op='reactor:notify_subscribers'
29
- const sql = compileGraphRules('posts', 'create', mockGraphRules.onCreate);
30
-
31
- expect(sql).toContain("INSERT INTO dzql_v2.events");
32
- expect(sql).toContain("'reactor:notify_subscribers'");
33
- // Simply check for the variable resolution logic
34
- expect(sql).toContain("v_result->>'id'");
35
- });
36
-
37
- test("should compile delete action to SQL delete", () => {
38
- // Should generate DELETE FROM comments ...
39
- const sql = compileGraphRules('posts', 'delete', mockGraphRules.onDelete);
40
-
41
- expect(sql).toContain("DELETE FROM comments");
42
- expect(sql).toContain("post_id = (v_old_data->>'id')::int"); // Variable resolution from old record
43
- });
44
-
45
- test("should compile create action with field defaults", () => {
46
- const rules: GraphRuleIR[] = [{
47
- trigger: 'create',
48
- action: 'create',
49
- target: 'memberships',
50
- params: { user_id: '@user_id', org_id: '@id', valid_from: '@today' }
51
- }];
52
-
53
- const sql = compileGraphRules('organisations', 'create', rules);
54
-
55
- expect(sql).toContain("INSERT INTO memberships");
56
- expect(sql).toContain("user_id, org_id, valid_from");
57
- expect(sql).toContain("p_user_id");
58
- expect(sql).toContain("(v_result->>'id')::int");
59
- expect(sql).toContain("CURRENT_DATE");
60
- });
61
-
62
- test("should compile update action with match clause", () => {
63
- const rules: GraphRuleIR[] = [{
64
- trigger: 'update',
65
- action: 'update',
66
- target: 'audit_log',
67
- params: { last_modified: '@now' },
68
- match: { entity_id: '@id' }
69
- }];
70
-
71
- const sql = compileGraphRules('posts', 'update', rules);
72
-
73
- expect(sql).toContain("UPDATE audit_log");
74
- expect(sql).toContain("SET last_modified = NOW()");
75
- expect(sql).toContain("WHERE entity_id = (v_result->>'id')::int");
76
- });
77
-
78
- test("should compile validate action", () => {
79
- const rules: GraphRuleIR[] = [{
80
- trigger: 'create',
81
- action: 'validate',
82
- target: 'check_quota',
83
- params: { user_id: '@user_id' },
84
- error_message: 'Quota exceeded'
85
- }];
86
-
87
- const sql = compileGraphRules('posts', 'create', rules);
88
-
89
- expect(sql).toContain("IF NOT check_quota(");
90
- expect(sql).toContain("user_id => p_user_id");
91
- expect(sql).toContain("RAISE EXCEPTION 'Quota exceeded'");
92
- });
93
-
94
- test("should compile execute action", () => {
95
- const rules: GraphRuleIR[] = [{
96
- trigger: 'create',
97
- action: 'execute',
98
- target: 'send_notification',
99
- params: { post_id: '@id', author_id: '@author_id' }
100
- }];
101
-
102
- const sql = compileGraphRules('posts', 'create', rules);
103
-
104
- expect(sql).toContain("PERFORM send_notification(");
105
- expect(sql).toContain("post_id => (v_result->>'id')");
106
- expect(sql).toContain("author_id => (v_result->>'author_id')");
107
- });
108
-
109
- test("should compile conditional rule with @before/@after", () => {
110
- const rules: GraphRuleIR[] = [{
111
- trigger: 'update',
112
- action: 'reactor',
113
- target: 'status_changed',
114
- condition: "@before.status = 'draft' AND @after.status = 'published'",
115
- params: { post_id: '@id' }
116
- }];
117
-
118
- const sql = compileGraphRules('posts', 'update', rules);
119
-
120
- expect(sql).toContain("-- Condition:");
121
- expect(sql).toContain("IF (v_old_data->>'status') = 'draft' AND (v_result->>'status') = 'published' THEN");
122
- expect(sql).toContain("INSERT INTO dzql_v2.events");
123
- expect(sql).toContain("END IF;");
124
- });
125
-
126
- test("should resolve @before variables in delete trigger", () => {
127
- const rules: GraphRuleIR[] = [{
128
- trigger: 'delete',
129
- action: 'execute',
130
- target: 'archive_post',
131
- params: { title: '@before.title', author_id: '@before.author_id' }
132
- }];
133
-
134
- const sql = compileGraphRules('posts', 'delete', rules);
135
-
136
- expect(sql).toContain("PERFORM archive_post(");
137
- expect(sql).toContain("title => (v_old_data->>'title')");
138
- // Note: execute/validate params don't get ::int cast - they're named parameters
139
- expect(sql).toContain("author_id => (v_old_data->>'author_id')");
140
- });
141
-
142
- test("should handle multiple actions", () => {
143
- const rules: GraphRuleIR[] = [
144
- {
145
- trigger: 'create',
146
- action: 'validate',
147
- target: 'check_permissions',
148
- params: { user_id: '@user_id' },
149
- error_message: 'Not allowed'
150
- },
151
- {
152
- trigger: 'create',
153
- action: 'create',
154
- target: 'audit_log',
155
- params: { entity_id: '@id', action: 'created' }
156
- },
157
- {
158
- trigger: 'create',
159
- action: 'reactor',
160
- target: 'notify_admins',
161
- params: { post_id: '@id' }
162
- }
163
- ];
164
-
165
- const sql = compileGraphRules('posts', 'create', rules);
166
-
167
- // Should have all three actions
168
- expect(sql).toContain("IF NOT check_permissions");
169
- expect(sql).toContain("INSERT INTO audit_log");
170
- expect(sql).toContain("reactor:notify_admins");
171
- });
172
-
173
- });
@@ -1,174 +0,0 @@
1
- import { describe, test, expect, beforeAll, afterAll } from "bun:test";
2
- import { V2TestDatabase } from "./setup.js";
3
- import { generateCoreSQL, generateEntitySQL, generateSchemaSQL } from "../../src/cli/codegen/sql.js";
4
- import { generateIR } from "../../src/cli/compiler/ir.js";
5
- import { entities } from "../../examples/blog.js";
6
-
7
- const blogDomain = { entities, subscribables: {} };
8
-
9
- describe("V2 Database Integration (Real Postgres)", () => {
10
- let db: V2TestDatabase;
11
- let sql: any;
12
-
13
- beforeAll(async () => {
14
- db = new V2TestDatabase();
15
- // This connects to localhost:5433 by default (from setup.ts)
16
- sql = await db.setup();
17
-
18
- try {
19
- // 1. Generate SQL
20
- const ir = generateIR(blogDomain);
21
- const coreSQL = generateCoreSQL();
22
- const postsSchema = generateSchemaSQL("posts", ir.entities.posts);
23
- const commentsSchema = generateSchemaSQL("comments", ir.entities.comments);
24
- const postsSQL = generateEntitySQL("posts", ir.entities.posts);
25
- const commentsSQL = generateEntitySQL("comments", ir.entities.comments);
26
-
27
- // 2. Apply SQL to the real DB
28
- await db.applySQL(coreSQL);
29
- await db.applySQL(postsSchema);
30
- await db.applySQL(commentsSchema);
31
- await db.applySQL(postsSQL);
32
- await db.applySQL(commentsSQL);
33
- } catch (e) {
34
- console.error("Setup failed:", e);
35
- throw e;
36
- }
37
- });
38
-
39
- afterAll(async () => {
40
- await db.teardown();
41
- });
42
-
43
- test("should save a post via compiled function (Atomic Upsert)", async () => {
44
- const userId = 1;
45
- const postData = { id: 1, title: "Real DB Test", content: "It works!", author_id: 1 };
46
-
47
- // Call the compiled PL/pgSQL function
48
- const result = await sql`
49
- SELECT dzql_v2.save_posts(${userId}, ${sql.json(postData)}) as data
50
- `;
51
-
52
- const saved = result[0].data;
53
- expect(saved.id).toBe(1);
54
- expect(saved.title).toBe("Real DB Test");
55
-
56
- // Verify persistence
57
- const rows = await sql`SELECT * FROM posts WHERE id = 1`;
58
- expect(rows.length).toBe(1);
59
- expect(rows[0].title).toBe("Real DB Test");
60
- });
61
-
62
- test("should enforce permissions (Inlined SQL)", async () => {
63
- const hackerId = 2;
64
- const postData = { id: 2, title: "Hacked", content: "Bad", author_id: 1 };
65
-
66
- // Should fail because @author_id (1) != @user_id (2)
67
- try {
68
- await sql`
69
- SELECT dzql_v2.save_posts(${hackerId}, ${sql.json(postData)})
70
- `;
71
- expect(true).toBe(false); // Should not fail
72
- } catch (e: any) {
73
- expect(e.message).toContain("permission_denied");
74
- }
75
- });
76
-
77
- test("should emit normalized row events (Commit Batching)", async () => {
78
- // We expect the 'insert' from the first test to be in the events table
79
- const events = await sql`
80
- SELECT * FROM dzql_v2.events
81
- WHERE table_name = 'posts'
82
- ORDER BY id DESC LIMIT 1
83
- `;
84
-
85
- expect(events.length).toBe(1);
86
- expect(events[0].op).toBe("insert");
87
- expect(events[0].data.title).toBe("Real DB Test");
88
- expect(events[0].old_data).toBeNull(); // Insert has no old data
89
- expect(events[0].commit_id).toBeDefined(); // Ensure commit ID was generated
90
- });
91
-
92
- test("should populate old_data on update", async () => {
93
- const userId = 1;
94
- // Update the existing post (id: 1)
95
- const updateData = { id: 1, title: "Updated Title" };
96
-
97
- await sql`
98
- SELECT dzql_v2.save_posts(${userId}, ${sql.json(updateData)})
99
- `;
100
-
101
- const events = await sql`
102
- SELECT * FROM dzql_v2.events
103
- WHERE table_name = 'posts' AND op = 'update'
104
- ORDER BY id DESC LIMIT 1
105
- `;
106
-
107
- expect(events.length).toBe(1);
108
- expect(events[0].op).toBe("update");
109
- expect(events[0].data.title).toBe("Updated Title");
110
- expect(events[0].old_data).not.toBeNull();
111
- expect(events[0].old_data.title).toBe("Real DB Test"); // Previous title
112
- });
113
-
114
- test("should trigger reactors (Graph Rules)", async () => {
115
- // The blog.ts example has a reactor 'notify_subscribers' on create
116
- const reactorEvents = await sql`
117
- SELECT * FROM dzql_v2.events
118
- WHERE op LIKE 'reactor:%'
119
- ORDER BY id DESC LIMIT 1
120
- `;
121
-
122
- expect(reactorEvents.length).toBe(1);
123
- expect(reactorEvents[0].op).toBe("reactor:notify_subscribers");
124
- expect(reactorEvents[0].data.post_id).toBe("1");
125
- });
126
-
127
- test("should delete a post via compiled function (Atomic Delete)", async () => {
128
- const userId = 1;
129
- // Call the compiled PL/pgSQL function
130
- const result = await sql`
131
- SELECT dzql_v2.delete_posts(${userId}, ${sql.json({ id: 1 })}) as data
132
- `;
133
-
134
- const deleted = result[0].data;
135
- expect(deleted.id).toBe(1);
136
-
137
- // Verify gone
138
- const rows = await sql`SELECT * FROM posts WHERE id = 1`;
139
- expect(rows.length).toBe(0);
140
-
141
- // Verify Event
142
- const events = await sql`
143
- SELECT * FROM dzql_v2.events
144
- WHERE table_name = 'posts' AND op = 'delete'
145
- ORDER BY id DESC LIMIT 1
146
- `;
147
- expect(events.length).toBe(1);
148
- expect(events[0].pk.id).toBe(1);
149
- });
150
-
151
- test("should get a post via compiled function", async () => {
152
- // Re-create post first
153
- await sql`INSERT INTO posts (id, title, author_id) VALUES (100, 'Get Test', 1)`;
154
-
155
- const userId = 1;
156
- const result = await sql`
157
- SELECT dzql_v2.get_posts(${userId}, ${sql.json({ id: 100 })}) as data
158
- `;
159
-
160
- expect(result[0].data.title).toBe("Get Test");
161
- });
162
-
163
- test("should search posts via compiled function", async () => {
164
- // Search
165
- const userId = 1;
166
- const result = await sql`
167
- SELECT dzql_v2.search_posts(${userId}, ${sql.json({ limit: 5 })}) as data
168
- `;
169
-
170
- expect(result[0].data).toBeArray();
171
- expect(result[0].data.length).toBeGreaterThan(0);
172
- expect(result[0].data[0].title).toBe("Get Test");
173
- });
174
- });
@@ -1,65 +0,0 @@
1
- import { describe, test, expect, beforeAll } from "bun:test";
2
- import { generateCoreSQL, generateEntitySQL } from "../../src/cli/codegen/sql.js";
3
- import { generateIR } from "../../src/cli/compiler/ir.js";
4
-
5
- // Load our blog example directly (bypassing loader for test isolation)
6
- import { entities } from "../../examples/blog.js";
7
-
8
- const blogDomain = { entities, subscribables: {} };
9
-
10
- describe("V2 End-to-End Compiler Integration", () => {
11
- let ir: any;
12
- let coreSQL: string;
13
- let postsSQL: string;
14
- let commentsSQL: string;
15
-
16
- beforeAll(() => {
17
- // 1. Generate IR from Domain Object
18
- ir = generateIR(blogDomain);
19
-
20
- // 2. Generate SQL
21
- coreSQL = generateCoreSQL();
22
- postsSQL = generateEntitySQL("posts", ir.entities.posts);
23
- commentsSQL = generateEntitySQL("comments", ir.entities.comments);
24
- });
25
-
26
- test("should generate valid IR for blog", () => {
27
- expect(ir.entities.posts).toBeDefined();
28
- expect(ir.entities.posts.primaryKey).toEqual(["id"]);
29
- // Check permission parsing
30
- expect(ir.entities.posts.permissions.create).toEqual(['@author_id == @user_id']);
31
- });
32
-
33
- test("should generate core SQL schema", () => {
34
- expect(coreSQL).toContain("CREATE SCHEMA IF NOT EXISTS dzql_v2");
35
- expect(coreSQL).toContain("CREATE TABLE IF NOT EXISTS dzql_v2.events");
36
- });
37
-
38
- test("should generate atomic UPSERT for posts", () => {
39
- // Check function signature
40
- expect(postsSQL).toContain("CREATE OR REPLACE FUNCTION dzql_v2.save_posts");
41
-
42
- // Check security
43
- expect(postsSQL).toContain("SECURITY DEFINER");
44
- expect(postsSQL).toContain("SET search_path = dzql_v2, public");
45
-
46
- // Check Update/Insert Branching (New V2 Pattern with composite PK support)
47
- expect(postsSQL).toContain("AND EXISTS(SELECT 1 FROM posts WHERE");
48
- expect(postsSQL).toContain("UPDATE posts SET");
49
- expect(postsSQL).toContain("INSERT INTO posts");
50
-
51
- // Check column handling in update
52
- expect(postsSQL).toContain("title = CASE WHEN (p_data ? 'title')");
53
- });
54
-
55
- test("should inline permission checks", () => {
56
- // Check that @author_id == @user_id was compiled correctly
57
- expect(postsSQL).toContain("IF NOT ((p_data->>'author_id')::int = p_user_id) THEN");
58
- });
59
-
60
- test("should compile graph rules", () => {
61
- // Check reactor trigger
62
- expect(postsSQL).toContain("INSERT INTO dzql_v2.events");
63
- expect(postsSQL).toContain("'reactor:notify_subscribers'");
64
- });
65
- });