dzql 0.4.5 → 0.4.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dzql",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
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",
@@ -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
+ }
@@ -267,7 +267,8 @@ CREATE OR REPLACE FUNCTION search_${this.tableName}(
267
267
  p_search TEXT DEFAULT NULL,
268
268
  p_sort JSONB DEFAULT NULL,
269
269
  p_page INT DEFAULT 1,
270
- p_limit INT DEFAULT 25
270
+ p_limit INT DEFAULT 25,
271
+ p_on_date TIMESTAMPTZ DEFAULT NULL
271
272
  ) RETURNS JSONB AS $$
272
273
  DECLARE
273
274
  v_data JSONB;
@@ -280,8 +281,10 @@ DECLARE
280
281
  v_filter JSONB;
281
282
  v_operator TEXT;
282
283
  v_value JSONB;
284
+ v_on_date TIMESTAMPTZ;
283
285
  BEGIN
284
286
  v_offset := (p_page - 1) * p_limit;
287
+ v_on_date := COALESCE(p_on_date, NOW());
285
288
 
286
289
  -- Extract sort parameters
287
290
  v_sort_field := COALESCE(p_sort->>'field', '${this.entity.labelField}');
@@ -329,8 +332,7 @@ BEGIN
329
332
  v_where_clause := v_where_clause || ' AND (${searchConditions})';
330
333
  END IF;
331
334
 
332
- -- Add temporal filter
333
- v_where_clause := v_where_clause || '${this._generateTemporalFilter().replace(/\n/g, ' ')}';
335
+ -- Add temporal filter${this._generateTemporalFilterForSearch()}
334
336
 
335
337
  -- Get total count
336
338
  EXECUTE format('SELECT COUNT(*) FROM ${this.tableName} WHERE %s', v_where_clause) INTO v_total;
@@ -754,7 +756,7 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
754
756
  }
755
757
 
756
758
  /**
757
- * Generate temporal filter
759
+ * Generate temporal filter for static SQL (GET, LOOKUP)
758
760
  * @private
759
761
  */
760
762
  _generateTemporalFilter() {
@@ -770,6 +772,23 @@ $$ LANGUAGE plpgsql SECURITY DEFINER;`;
770
772
  AND (${validTo} > COALESCE(p_on_date, NOW()) OR ${validTo} IS NULL)`;
771
773
  }
772
774
 
775
+ /**
776
+ * Generate temporal filter for SEARCH function (dynamic SQL with EXECUTE)
777
+ * Uses format() to properly interpolate the v_on_date variable
778
+ * @private
779
+ */
780
+ _generateTemporalFilterForSearch() {
781
+ if (!this.entity.temporalFields || Object.keys(this.entity.temporalFields).length === 0) {
782
+ return '';
783
+ }
784
+
785
+ const validFrom = this.entity.temporalFields.valid_from || 'valid_from';
786
+ const validTo = this.entity.temporalFields.valid_to || 'valid_to';
787
+
788
+ return `
789
+ v_where_clause := v_where_clause || format(' AND ${validFrom} <= %L AND (${validTo} > %L OR ${validTo} IS NULL)', v_on_date, v_on_date);`;
790
+ }
791
+
773
792
  /**
774
793
  * Check if a trigger has any rules with actions
775
794
  * @private
@@ -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
- name text not null,
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
- create or replace function register_user(p_email text, p_password text)
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
- user_id int;
26
- salt text;
27
- hash text;
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
- salt := gen_salt('bf', 10);
31
- hash := crypt(p_password, salt);
35
+ v_salt := gen_salt('bf', 10);
36
+ v_hash := crypt(p_password, v_salt);
32
37
 
33
- -- Insert user
34
- insert into users (email, name, password_hash)
35
- values (p_email, split_part(p_email, '@', 1), hash)
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
- return _profile(user_id);
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, name, password_hash
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
- 'user_id', id,
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
  $$;