dzql 0.4.4 → 0.4.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.
- package/docs/compiler/README.md +2 -8
- package/docs/examples/README.md +38 -0
- package/docs/examples/blog.sql +160 -0
- package/docs/examples/venue-detail-simple.sql +8 -0
- package/docs/examples/venue-detail-subscribable.sql +45 -0
- package/docs/guides/subscriptions.md +2 -2
- package/package.json +2 -1
- package/src/compiler/codegen/auth-codegen.js +147 -0
- package/src/compiler/compiler.js +7 -0
- package/src/database/migrations/006_auth.sql +33 -22
- package/docs/compiler/OVERNIGHT_BUILD.md +0 -474
- package/docs/compiler/SESSION_SUMMARY.md +0 -266
- package/docs/compiler/SUMMARY.md +0 -528
- package/docs/compiler/dzql-compiler-m2m-change-request 2.md +0 -562
- package/docs/compiler/dzql-compiler-m2m-change-request.md +0 -375
package/docs/compiler/README.md
CHANGED
|
@@ -6,17 +6,11 @@ The DZQL Compiler transforms declarative entity definitions into optimized Postg
|
|
|
6
6
|
|
|
7
7
|
- **[Quickstart Guide](QUICKSTART.md)** - Get started with the compiler in 5 minutes
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Reference
|
|
10
10
|
|
|
11
11
|
- **[Advanced Filters](ADVANCED_FILTERS.md)** - Complex search operators and patterns
|
|
12
12
|
- **[Coding Standards](CODING_STANDARDS.md)** - Best practices for DZQL code
|
|
13
|
-
|
|
14
|
-
## Reference
|
|
15
|
-
|
|
16
|
-
- **[Comparison](COMPARISON.md)** - How DZQL compares to other approaches
|
|
17
|
-
- **[Session Summary](SESSION_SUMMARY.md)** - Development session documentation
|
|
18
|
-
- **[Summary](SUMMARY.md)** - Compiler overview and architecture
|
|
19
|
-
- **[Overnight Build](OVERNIGHT_BUILD.md)** - Batch compilation process
|
|
13
|
+
- **[Comparison](COMPARISON.md)** - Runtime vs compiled side-by-side
|
|
20
14
|
|
|
21
15
|
## Using the Compiler
|
|
22
16
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# DZQL Examples
|
|
2
|
+
|
|
3
|
+
SQL examples demonstrating DZQL entity registration patterns.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
### blog.sql
|
|
8
|
+
A complete blog application with:
|
|
9
|
+
- Multiple entities (users, posts, comments, tags)
|
|
10
|
+
- Many-to-many relationships (posts ↔ tags)
|
|
11
|
+
- Soft delete
|
|
12
|
+
- FK includes
|
|
13
|
+
- Permission paths
|
|
14
|
+
- Notification paths
|
|
15
|
+
|
|
16
|
+
### venue-detail-simple.sql
|
|
17
|
+
Basic subscribable definition for venue data.
|
|
18
|
+
|
|
19
|
+
### venue-detail-subscribable.sql
|
|
20
|
+
Full subscribable with relations and permission paths. Demonstrates:
|
|
21
|
+
- Root entity with FK includes
|
|
22
|
+
- Child entity filtering
|
|
23
|
+
- Permission path syntax
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
These files are meant to be run after DZQL core migrations. See the [Tutorial](../getting-started/tutorial.md) for complete setup instructions.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# After setting up your database with DZQL migrations
|
|
31
|
+
psql $DATABASE_URL < examples/blog.sql
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## See Also
|
|
35
|
+
|
|
36
|
+
- [API Reference](../reference/api.md) - Entity registration parameters
|
|
37
|
+
- [Many-to-Many Guide](../guides/many-to-many.md) - M2M configuration
|
|
38
|
+
- [Subscriptions Guide](../guides/subscriptions.md) - Subscribable patterns
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- Blog Application Example
|
|
3
|
+
-- ============================================================================
|
|
4
|
+
--
|
|
5
|
+
-- This example demonstrates:
|
|
6
|
+
-- - Multiple related entities (users, posts, comments, tags)
|
|
7
|
+
-- - Many-to-many relationships (posts <-> tags via post_tags junction)
|
|
8
|
+
-- - Soft delete (posts.deleted_at)
|
|
9
|
+
-- - FK includes (dereferencing author, post)
|
|
10
|
+
-- - Permission paths (author can edit/delete own content)
|
|
11
|
+
-- - Notification paths (notify post author when comments added)
|
|
12
|
+
--
|
|
13
|
+
-- To use this example:
|
|
14
|
+
-- 1. Create tables first (see CREATE TABLE statements below)
|
|
15
|
+
-- 2. Run the dzql.register_entity() calls to enable CRUD operations
|
|
16
|
+
-- 3. Use the generated API: save_posts, get_posts, search_posts, etc.
|
|
17
|
+
--
|
|
18
|
+
-- For a complete working example with Docker, tests, and frontend:
|
|
19
|
+
-- See packages/blog/ in the DZQL repository
|
|
20
|
+
--
|
|
21
|
+
-- ============================================================================
|
|
22
|
+
|
|
23
|
+
-- Create tables
|
|
24
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
25
|
+
id SERIAL PRIMARY KEY,
|
|
26
|
+
name VARCHAR(255) NOT NULL,
|
|
27
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
28
|
+
password_hash TEXT NOT NULL,
|
|
29
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS posts (
|
|
33
|
+
id SERIAL PRIMARY KEY,
|
|
34
|
+
title VARCHAR(500) NOT NULL,
|
|
35
|
+
content TEXT NOT NULL,
|
|
36
|
+
summary TEXT,
|
|
37
|
+
author_id INT REFERENCES users(id),
|
|
38
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
39
|
+
deleted_at TIMESTAMP
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS comments (
|
|
43
|
+
id SERIAL PRIMARY KEY,
|
|
44
|
+
content TEXT NOT NULL,
|
|
45
|
+
post_id INT REFERENCES posts(id),
|
|
46
|
+
author_id INT REFERENCES users(id),
|
|
47
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
-- Tags table for categorizing posts
|
|
51
|
+
CREATE TABLE IF NOT EXISTS tags (
|
|
52
|
+
id SERIAL PRIMARY KEY,
|
|
53
|
+
name VARCHAR(50) UNIQUE NOT NULL,
|
|
54
|
+
color VARCHAR(7) DEFAULT '#3788d8'
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
-- Junction table for post-tag many-to-many relationship
|
|
58
|
+
CREATE TABLE IF NOT EXISTS post_tags (
|
|
59
|
+
post_id INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
|
|
60
|
+
tag_id INT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
|
61
|
+
PRIMARY KEY (post_id, tag_id)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_post_tags_post_id ON post_tags(post_id);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_post_tags_tag_id ON post_tags(tag_id);
|
|
66
|
+
|
|
67
|
+
-- ============================================================================
|
|
68
|
+
-- DZQL Entity Registrations
|
|
69
|
+
-- ============================================================================
|
|
70
|
+
|
|
71
|
+
-- Users entity - blog authors
|
|
72
|
+
select dzql.register_entity(
|
|
73
|
+
'users',
|
|
74
|
+
'name',
|
|
75
|
+
array['name', 'email'],
|
|
76
|
+
'{}', -- no FK includes
|
|
77
|
+
false, -- hard delete
|
|
78
|
+
'{}', -- no reverse FK
|
|
79
|
+
'{}', -- no notifications
|
|
80
|
+
jsonb_build_object(
|
|
81
|
+
'view', array[]::text[], -- Anyone can view users
|
|
82
|
+
'create', array[]::text[], -- Anyone can register
|
|
83
|
+
'update', array['@id'], -- Only update own profile
|
|
84
|
+
'delete', array['@id'] -- Only delete own account
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
-- Register tags entity first
|
|
89
|
+
select dzql.register_entity(
|
|
90
|
+
'tags',
|
|
91
|
+
'name',
|
|
92
|
+
array['name'],
|
|
93
|
+
'{}', -- no FK includes
|
|
94
|
+
false, -- hard delete
|
|
95
|
+
'{}', -- no temporal
|
|
96
|
+
'{}', -- no notifications
|
|
97
|
+
jsonb_build_object(
|
|
98
|
+
'view', array[]::text[], -- Anyone can view tags
|
|
99
|
+
'create', array[]::text[], -- Anyone can create tags
|
|
100
|
+
'update', array[]::text[], -- Anyone can update tags
|
|
101
|
+
'delete', array[]::text[] -- Anyone can delete tags
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
-- Posts entity - blog posts with M2M tags
|
|
106
|
+
select dzql.register_entity(
|
|
107
|
+
'posts',
|
|
108
|
+
'title',
|
|
109
|
+
array['title', 'content', 'summary'],
|
|
110
|
+
jsonb_build_object(
|
|
111
|
+
'author', 'users' -- FK to users
|
|
112
|
+
),
|
|
113
|
+
true, -- soft delete (deleted_at)
|
|
114
|
+
'{}', -- no temporal
|
|
115
|
+
'{}', -- no notification paths
|
|
116
|
+
jsonb_build_object(
|
|
117
|
+
'view', array[]::text[], -- Anyone can view posts
|
|
118
|
+
'create', array[]::text[], -- Anyone can create posts
|
|
119
|
+
'update', array['@author_id'], -- Only author can update
|
|
120
|
+
'delete', array['@author_id'] -- Only author can delete
|
|
121
|
+
),
|
|
122
|
+
jsonb_build_object(
|
|
123
|
+
'many_to_many', jsonb_build_object(
|
|
124
|
+
'tags', jsonb_build_object(
|
|
125
|
+
'junction_table', 'post_tags',
|
|
126
|
+
'local_key', 'post_id',
|
|
127
|
+
'foreign_key', 'tag_id',
|
|
128
|
+
'target_entity', 'tags',
|
|
129
|
+
'id_field', 'tag_ids',
|
|
130
|
+
'expand', true -- Include full tag objects in response
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
) -- graph_rules with M2M
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
-- Comments entity - blog comments
|
|
137
|
+
select dzql.register_entity(
|
|
138
|
+
'comments',
|
|
139
|
+
'content',
|
|
140
|
+
array['content'],
|
|
141
|
+
jsonb_build_object(
|
|
142
|
+
'post', 'posts',
|
|
143
|
+
'author', 'users'
|
|
144
|
+
),
|
|
145
|
+
false, -- hard delete
|
|
146
|
+
'{}', -- no reverse FK
|
|
147
|
+
jsonb_build_object(
|
|
148
|
+
'post_author', array['@post_id->posts.author_id'],
|
|
149
|
+
'commenters', array['@post_id']
|
|
150
|
+
),
|
|
151
|
+
jsonb_build_object(
|
|
152
|
+
'view', array[]::text[], -- Anyone can view comments
|
|
153
|
+
'create', array[]::text[], -- Anyone can comment
|
|
154
|
+
'update', array['@author_id'], -- Only author can update
|
|
155
|
+
'delete', array[
|
|
156
|
+
'@author_id', -- Author can delete
|
|
157
|
+
'@post_id->posts.author_id' -- Post author can delete
|
|
158
|
+
]
|
|
159
|
+
)
|
|
160
|
+
);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- Simplified test subscribable for initial testing
|
|
2
|
+
SELECT dzql.register_subscribable(
|
|
3
|
+
'venue_detail',
|
|
4
|
+
'{"subscribe": ["@org_id->acts_for[org_id=$]{active}.user_id"]}'::jsonb,
|
|
5
|
+
'{"venue_id": "int"}'::jsonb,
|
|
6
|
+
'venues',
|
|
7
|
+
'{"org": "organisations", "sites": {"entity": "sites", "filter": "venue_id=$venue_id"}}'::jsonb
|
|
8
|
+
);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
-- Example: Venue Detail Subscribable
|
|
2
|
+
-- This subscribable builds a denormalized document containing:
|
|
3
|
+
-- - The venue record
|
|
4
|
+
-- - The organization (FK expanded)
|
|
5
|
+
-- - All sites belonging to the venue
|
|
6
|
+
-- - All packages with their allocations
|
|
7
|
+
|
|
8
|
+
SELECT dzql.register_subscribable(
|
|
9
|
+
'venue_detail',
|
|
10
|
+
|
|
11
|
+
-- Permission: who can subscribe?
|
|
12
|
+
-- Users who act_for the venue's organization (active roles only)
|
|
13
|
+
jsonb_build_object(
|
|
14
|
+
'subscribe', ARRAY['@org_id->acts_for[org_id=$]{active}.user_id']
|
|
15
|
+
),
|
|
16
|
+
|
|
17
|
+
-- Parameters: subscription key
|
|
18
|
+
jsonb_build_object(
|
|
19
|
+
'venue_id', 'int'
|
|
20
|
+
),
|
|
21
|
+
|
|
22
|
+
-- Root entity
|
|
23
|
+
'venues',
|
|
24
|
+
|
|
25
|
+
-- Relations to include in document
|
|
26
|
+
jsonb_build_object(
|
|
27
|
+
-- FK expansion: organisation
|
|
28
|
+
'org', 'organisations',
|
|
29
|
+
|
|
30
|
+
-- Child collection: sites
|
|
31
|
+
'sites', jsonb_build_object(
|
|
32
|
+
'entity', 'sites',
|
|
33
|
+
'filter', 'venue_id=$venue_id'
|
|
34
|
+
),
|
|
35
|
+
|
|
36
|
+
-- Child collection: packages with nested allocations
|
|
37
|
+
'packages', jsonb_build_object(
|
|
38
|
+
'entity', 'packages',
|
|
39
|
+
'filter', 'venue_id=$venue_id',
|
|
40
|
+
'include', jsonb_build_object(
|
|
41
|
+
'allocations', 'allocations'
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
);
|
|
@@ -28,7 +28,7 @@ Live Query Subscriptions (Pattern 1 from vision.md) enable clients to subscribe
|
|
|
28
28
|
Create a SQL file with your subscribable definition:
|
|
29
29
|
|
|
30
30
|
```sql
|
|
31
|
-
-- examples/
|
|
31
|
+
-- examples/venue-detail-subscribable.sql
|
|
32
32
|
SELECT dzql.register_subscribable(
|
|
33
33
|
'venue_detail',
|
|
34
34
|
'{"subscribe": ["@org_id->acts_for[org_id=$]{active}.user_id"]}'::jsonb,
|
|
@@ -56,7 +56,7 @@ SELECT dzql.register_subscribable(
|
|
|
56
56
|
```bash
|
|
57
57
|
# Compile subscribable to SQL functions
|
|
58
58
|
bun packages/dzql/src/compiler/cli/compile-subscribable.js \
|
|
59
|
-
examples/
|
|
59
|
+
examples/venue-detail-subscribable.sql \
|
|
60
60
|
> /tmp/venue_detail.sql
|
|
61
61
|
|
|
62
62
|
# Deploy to database
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dzql",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "PostgreSQL-powered framework with zero boilerplate CRUD operations and real-time WebSocket synchronization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server/index.js",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"src/**/*.js",
|
|
20
20
|
"src/database/migrations/**/*.sql",
|
|
21
21
|
"docs/**/*.md",
|
|
22
|
+
"docs/examples/*.sql",
|
|
22
23
|
"README.md",
|
|
23
24
|
"LICENSE"
|
|
24
25
|
],
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Code Generator
|
|
3
|
+
* Generates PostgreSQL functions for user authentication
|
|
4
|
+
* Only generated when the entity is named 'users'
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class AuthCodegen {
|
|
8
|
+
constructor(entity) {
|
|
9
|
+
this.entity = entity;
|
|
10
|
+
this.tableName = entity.tableName;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if this entity should have auth functions generated
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
shouldGenerate() {
|
|
18
|
+
return this.tableName === 'users';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate all auth functions
|
|
23
|
+
* @returns {string} SQL for auth functions
|
|
24
|
+
*/
|
|
25
|
+
generateAll() {
|
|
26
|
+
if (!this.shouldGenerate()) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return [
|
|
31
|
+
this._generateProfileFunction(),
|
|
32
|
+
this._generateRegisterFunction(),
|
|
33
|
+
this._generateLoginFunction()
|
|
34
|
+
].join('\n\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate _profile function
|
|
39
|
+
* Returns all user columns except sensitive fields
|
|
40
|
+
* @private
|
|
41
|
+
*/
|
|
42
|
+
_generateProfileFunction() {
|
|
43
|
+
return `-- ============================================================================
|
|
44
|
+
-- Auth: _profile function for ${this.tableName}
|
|
45
|
+
-- Returns user record minus sensitive fields
|
|
46
|
+
-- ============================================================================
|
|
47
|
+
CREATE OR REPLACE FUNCTION _profile(p_user_id INT)
|
|
48
|
+
RETURNS JSONB
|
|
49
|
+
LANGUAGE SQL
|
|
50
|
+
SECURITY DEFINER
|
|
51
|
+
AS $$
|
|
52
|
+
SELECT jsonb_build_object('user_id', u.id) || (to_jsonb(u.*) - 'id' - 'password_hash' - 'password' - 'secret' - 'token')
|
|
53
|
+
FROM ${this.tableName} u
|
|
54
|
+
WHERE id = p_user_id;
|
|
55
|
+
$$;`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate register_user function
|
|
60
|
+
* Supports optional extra fields via JSON parameter
|
|
61
|
+
* @private
|
|
62
|
+
*/
|
|
63
|
+
_generateRegisterFunction() {
|
|
64
|
+
return `-- ============================================================================
|
|
65
|
+
-- Auth: register_user function for ${this.tableName}
|
|
66
|
+
-- p_extra: optional JSON object with additional fields to set on the user record
|
|
67
|
+
-- Example: register_user('test@example.com', 'password', '{"name": "Test User"}')
|
|
68
|
+
-- ============================================================================
|
|
69
|
+
CREATE OR REPLACE FUNCTION register_user(p_email TEXT, p_password TEXT, p_extra JSONB DEFAULT '{}')
|
|
70
|
+
RETURNS JSONB
|
|
71
|
+
LANGUAGE plpgsql
|
|
72
|
+
SECURITY DEFINER
|
|
73
|
+
AS $$
|
|
74
|
+
DECLARE
|
|
75
|
+
v_user_id INT;
|
|
76
|
+
v_salt TEXT;
|
|
77
|
+
v_hash TEXT;
|
|
78
|
+
v_insert_data JSONB;
|
|
79
|
+
BEGIN
|
|
80
|
+
-- Generate salt and hash password
|
|
81
|
+
v_salt := gen_salt('bf', 10);
|
|
82
|
+
v_hash := crypt(p_password, v_salt);
|
|
83
|
+
|
|
84
|
+
-- Build insert data: extra fields + email + password_hash (extra cannot override core fields)
|
|
85
|
+
v_insert_data := (p_extra - 'id' - 'email' - 'password_hash' - 'password')
|
|
86
|
+
|| jsonb_build_object('email', p_email, 'password_hash', v_hash);
|
|
87
|
+
|
|
88
|
+
-- Dynamic INSERT from JSONB (same pattern as compiled save functions)
|
|
89
|
+
EXECUTE (
|
|
90
|
+
SELECT format(
|
|
91
|
+
'INSERT INTO ${this.tableName} (%s) VALUES (%s) RETURNING id',
|
|
92
|
+
string_agg(quote_ident(key), ', '),
|
|
93
|
+
string_agg(quote_nullable(value), ', ')
|
|
94
|
+
)
|
|
95
|
+
FROM jsonb_each_text(v_insert_data) kv(key, value)
|
|
96
|
+
) INTO v_user_id;
|
|
97
|
+
|
|
98
|
+
RETURN _profile(v_user_id);
|
|
99
|
+
EXCEPTION
|
|
100
|
+
WHEN unique_violation THEN
|
|
101
|
+
RAISE EXCEPTION 'Email already exists' USING errcode = '23505';
|
|
102
|
+
END $$;`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generate login_user function
|
|
107
|
+
* @private
|
|
108
|
+
*/
|
|
109
|
+
_generateLoginFunction() {
|
|
110
|
+
return `-- ============================================================================
|
|
111
|
+
-- Auth: login_user function for ${this.tableName}
|
|
112
|
+
-- ============================================================================
|
|
113
|
+
CREATE OR REPLACE FUNCTION login_user(p_email TEXT, p_password TEXT)
|
|
114
|
+
RETURNS JSONB
|
|
115
|
+
LANGUAGE plpgsql
|
|
116
|
+
SECURITY DEFINER
|
|
117
|
+
AS $$
|
|
118
|
+
DECLARE
|
|
119
|
+
v_user_record RECORD;
|
|
120
|
+
BEGIN
|
|
121
|
+
SELECT id, email, password_hash
|
|
122
|
+
INTO v_user_record
|
|
123
|
+
FROM ${this.tableName}
|
|
124
|
+
WHERE email = p_email;
|
|
125
|
+
|
|
126
|
+
IF NOT FOUND THEN
|
|
127
|
+
RAISE EXCEPTION 'Invalid credentials' USING errcode = '28000';
|
|
128
|
+
END IF;
|
|
129
|
+
|
|
130
|
+
IF NOT (v_user_record.password_hash = crypt(p_password, v_user_record.password_hash)) THEN
|
|
131
|
+
RAISE EXCEPTION 'Invalid credentials' USING errcode = '28000';
|
|
132
|
+
END IF;
|
|
133
|
+
|
|
134
|
+
RETURN _profile(v_user_record.id);
|
|
135
|
+
END $$;`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generate auth functions for an entity (only if it's the users table)
|
|
141
|
+
* @param {Object} entity - Entity configuration
|
|
142
|
+
* @returns {string} SQL for auth functions (empty string if not users table)
|
|
143
|
+
*/
|
|
144
|
+
export function generateAuthFunctions(entity) {
|
|
145
|
+
const codegen = new AuthCodegen(entity);
|
|
146
|
+
return codegen.generateAll();
|
|
147
|
+
}
|
package/src/compiler/compiler.js
CHANGED
|
@@ -10,6 +10,7 @@ import { generateOperations } from './codegen/operation-codegen.js';
|
|
|
10
10
|
import { generateNotificationFunction } from './codegen/notification-codegen.js';
|
|
11
11
|
import { generateGraphRuleFunctions } from './codegen/graph-rules-codegen.js';
|
|
12
12
|
import { generateSubscribable } from './codegen/subscribable-codegen.js';
|
|
13
|
+
import { generateAuthFunctions } from './codegen/auth-codegen.js';
|
|
13
14
|
import crypto from 'crypto';
|
|
14
15
|
|
|
15
16
|
export class DZQLCompiler {
|
|
@@ -53,6 +54,12 @@ export class DZQLCompiler {
|
|
|
53
54
|
const operationSQL = generateOperations(normalizedEntity);
|
|
54
55
|
sections.push(operationSQL);
|
|
55
56
|
|
|
57
|
+
// Auth functions (only for users table)
|
|
58
|
+
const authSQL = generateAuthFunctions(normalizedEntity);
|
|
59
|
+
if (authSQL) {
|
|
60
|
+
sections.push(authSQL);
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
// Notification path resolution (if needed)
|
|
57
64
|
if (normalizedEntity.notificationPaths &&
|
|
58
65
|
Object.keys(normalizedEntity.notificationPaths).length > 0) {
|
|
@@ -5,37 +5,51 @@
|
|
|
5
5
|
create extension if not exists pgcrypto;
|
|
6
6
|
|
|
7
7
|
-- === Users Table ===
|
|
8
|
+
-- Core auth table with optional name field
|
|
9
|
+
-- Applications can add additional columns as needed
|
|
10
|
+
-- Note: created_at is tracked via the action log, not here
|
|
8
11
|
create table if not exists users (
|
|
9
12
|
id serial primary key,
|
|
13
|
+
name text,
|
|
10
14
|
email text unique not null,
|
|
11
|
-
|
|
12
|
-
password_hash text not null,
|
|
13
|
-
created_at timestamptz default now()
|
|
15
|
+
password_hash text not null
|
|
14
16
|
);
|
|
15
17
|
|
|
16
18
|
-- === Auth Functions ===
|
|
17
19
|
|
|
18
20
|
-- Register new user
|
|
19
|
-
|
|
21
|
+
-- p_extra: optional JSON object with additional fields to set on the user record
|
|
22
|
+
-- Example: register_user('test@example.com', 'password', '{"name": "Test User"}')
|
|
23
|
+
create or replace function register_user(p_email text, p_password text, p_extra jsonb default '{}')
|
|
20
24
|
returns jsonb
|
|
21
25
|
language plpgsql
|
|
22
26
|
security definer
|
|
23
27
|
as $$
|
|
24
28
|
declare
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
v_user_id int;
|
|
30
|
+
v_salt text;
|
|
31
|
+
v_hash text;
|
|
32
|
+
v_insert_data jsonb;
|
|
28
33
|
begin
|
|
29
34
|
-- Generate salt and hash password
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
v_salt := gen_salt('bf', 10);
|
|
36
|
+
v_hash := crypt(p_password, v_salt);
|
|
32
37
|
|
|
33
|
-
--
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
returning id into user_id;
|
|
38
|
+
-- Build insert data: extra fields + email + password_hash (extra cannot override core fields)
|
|
39
|
+
v_insert_data := (p_extra - 'id' - 'email' - 'password_hash' - 'password')
|
|
40
|
+
|| jsonb_build_object('email', p_email, 'password_hash', v_hash);
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
-- Dynamic INSERT from JSONB (same pattern as compiled save functions)
|
|
43
|
+
execute (
|
|
44
|
+
select format(
|
|
45
|
+
'INSERT INTO users (%s) VALUES (%s) RETURNING id',
|
|
46
|
+
string_agg(quote_ident(key), ', '),
|
|
47
|
+
string_agg(quote_nullable(value), ', ')
|
|
48
|
+
)
|
|
49
|
+
from jsonb_each_text(v_insert_data) kv(key, value)
|
|
50
|
+
) into v_user_id;
|
|
51
|
+
|
|
52
|
+
return _profile(v_user_id);
|
|
39
53
|
exception
|
|
40
54
|
when unique_violation then
|
|
41
55
|
raise exception 'Email already exists' using errcode = '23505';
|
|
@@ -50,7 +64,7 @@ as $$
|
|
|
50
64
|
declare
|
|
51
65
|
user_record record;
|
|
52
66
|
begin
|
|
53
|
-
select id, email,
|
|
67
|
+
select id, email, password_hash
|
|
54
68
|
into user_record
|
|
55
69
|
from users
|
|
56
70
|
where email = p_email;
|
|
@@ -67,17 +81,14 @@ begin
|
|
|
67
81
|
end $$;
|
|
68
82
|
|
|
69
83
|
-- Get user profile (private function)
|
|
84
|
+
-- Returns all user columns except sensitive fields (password_hash, password, secret, token)
|
|
85
|
+
-- This allows the users table to have any additional columns without modifying this function
|
|
70
86
|
create or replace function _profile(p_user_id int)
|
|
71
87
|
returns jsonb
|
|
72
88
|
language sql
|
|
73
89
|
security definer
|
|
74
90
|
as $$
|
|
75
|
-
select jsonb_build_object(
|
|
76
|
-
|
|
77
|
-
'email', email,
|
|
78
|
-
'name', name,
|
|
79
|
-
'created_at', created_at
|
|
80
|
-
)
|
|
81
|
-
from users
|
|
91
|
+
select jsonb_build_object('user_id', u.id) || (to_jsonb(u.*) - 'id' - 'password_hash' - 'password' - 'secret' - 'token')
|
|
92
|
+
from users u
|
|
82
93
|
where id = p_user_id;
|
|
83
94
|
$$;
|