dzql 0.4.8 → 0.5.2
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/bin/cli.js +41 -20
- package/docs/compiler/CODING_STANDARDS.md +66 -0
- package/docs/for-ai/claude-guide.md +21 -18
- package/docs/reference/api.md +74 -7
- package/docs/reference/client.md +41 -0
- package/package.json +1 -1
- package/src/client/ws.js +7 -0
- package/src/compiler/codegen/auth-codegen.js +12 -6
- package/src/database/migrations/006_auth.sql +8 -8
- package/src/server/db.js +8 -2
- package/src/server/ws.js +2 -0
package/bin/cli.js
CHANGED
|
@@ -230,33 +230,49 @@ CREATE TRIGGER dzql_events_notify
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
// Generate auth functions (required for WebSocket server)
|
|
233
|
-
|
|
233
|
+
// This is a fallback for when there's no users entity - otherwise users.sql has these
|
|
234
|
+
const authSQL = `-- Authentication Functions (fallback)
|
|
234
235
|
-- Required for DZQL WebSocket server
|
|
236
|
+
-- Note: If you have a users entity, auth functions are in users.sql instead
|
|
235
237
|
|
|
236
238
|
-- Enable pgcrypto extension for password hashing
|
|
237
239
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
238
240
|
|
|
239
241
|
-- Register new user
|
|
240
|
-
|
|
242
|
+
-- p_options: optional JSON object with additional fields to set on the user record
|
|
243
|
+
CREATE OR REPLACE FUNCTION register_user(p_email TEXT, p_password TEXT, p_options JSON DEFAULT NULL)
|
|
241
244
|
RETURNS JSONB
|
|
242
245
|
LANGUAGE plpgsql
|
|
243
246
|
SECURITY DEFINER
|
|
244
247
|
AS $$
|
|
245
248
|
DECLARE
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
+
v_user_id INT;
|
|
250
|
+
v_salt TEXT;
|
|
251
|
+
v_hash TEXT;
|
|
252
|
+
v_insert_data JSONB;
|
|
249
253
|
BEGIN
|
|
250
254
|
-- Generate salt and hash password
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
--
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
255
|
+
v_salt := gen_salt('bf', 10);
|
|
256
|
+
v_hash := crypt(p_password, v_salt);
|
|
257
|
+
|
|
258
|
+
-- Build insert data: options fields + email + password_hash
|
|
259
|
+
-- Cast p_options to JSONB for internal operations (JSON type is for API boundary convenience)
|
|
260
|
+
v_insert_data := jsonb_build_object('email', p_email, 'password_hash', v_hash);
|
|
261
|
+
IF p_options IS NOT NULL THEN
|
|
262
|
+
v_insert_data := (p_options::jsonb - 'id' - 'email' - 'password_hash' - 'password') || v_insert_data;
|
|
263
|
+
END IF;
|
|
258
264
|
|
|
259
|
-
|
|
265
|
+
-- Dynamic INSERT from JSONB
|
|
266
|
+
EXECUTE (
|
|
267
|
+
SELECT format(
|
|
268
|
+
'INSERT INTO users (%s) VALUES (%s) RETURNING id',
|
|
269
|
+
string_agg(quote_ident(key), ', '),
|
|
270
|
+
string_agg(quote_nullable(value), ', ')
|
|
271
|
+
)
|
|
272
|
+
FROM jsonb_each_text(v_insert_data) kv(key, value)
|
|
273
|
+
) INTO v_user_id;
|
|
274
|
+
|
|
275
|
+
RETURN _profile(v_user_id);
|
|
260
276
|
EXCEPTION
|
|
261
277
|
WHEN unique_violation THEN
|
|
262
278
|
RAISE EXCEPTION 'Email already exists' USING errcode = '23505';
|
|
@@ -269,10 +285,10 @@ LANGUAGE plpgsql
|
|
|
269
285
|
SECURITY DEFINER
|
|
270
286
|
AS $$
|
|
271
287
|
DECLARE
|
|
272
|
-
|
|
288
|
+
v_user_record RECORD;
|
|
273
289
|
BEGIN
|
|
274
|
-
SELECT id, email,
|
|
275
|
-
INTO
|
|
290
|
+
SELECT id, email, password_hash
|
|
291
|
+
INTO v_user_record
|
|
276
292
|
FROM users
|
|
277
293
|
WHERE email = p_email;
|
|
278
294
|
|
|
@@ -280,14 +296,15 @@ BEGIN
|
|
|
280
296
|
RAISE EXCEPTION 'Invalid credentials' USING errcode = '28000';
|
|
281
297
|
END IF;
|
|
282
298
|
|
|
283
|
-
IF NOT (
|
|
299
|
+
IF NOT (v_user_record.password_hash = crypt(p_password, v_user_record.password_hash)) THEN
|
|
284
300
|
RAISE EXCEPTION 'Invalid credentials' USING errcode = '28000';
|
|
285
301
|
END IF;
|
|
286
302
|
|
|
287
|
-
RETURN _profile(
|
|
303
|
+
RETURN _profile(v_user_record.id);
|
|
288
304
|
END $$;
|
|
289
305
|
|
|
290
306
|
-- Get user profile (private function, called after login/register)
|
|
307
|
+
-- Returns all columns except sensitive fields
|
|
291
308
|
CREATE OR REPLACE FUNCTION _profile(p_user_id INT)
|
|
292
309
|
RETURNS JSONB
|
|
293
310
|
LANGUAGE sql
|
|
@@ -299,8 +316,12 @@ AS $$
|
|
|
299
316
|
$$;
|
|
300
317
|
`;
|
|
301
318
|
|
|
302
|
-
|
|
303
|
-
|
|
319
|
+
// Only generate 002_auth.sql if there's no users entity (which has its own auth functions)
|
|
320
|
+
const hasUsersEntity = result.results.some(r => r.tableName === 'users');
|
|
321
|
+
if (!hasUsersEntity) {
|
|
322
|
+
writeFileSync(resolve(options.output, '002_auth.sql'), authSQL, 'utf-8');
|
|
323
|
+
console.log(` ✓ 002_auth.sql`);
|
|
324
|
+
}
|
|
304
325
|
|
|
305
326
|
const checksums = {};
|
|
306
327
|
|
|
@@ -72,6 +72,72 @@ CREATE FUNCTION lookup_users(p_user_id INT, p_filter TEXT, ...)
|
|
|
72
72
|
CREATE FUNCTION search_users(p_user_id INT, p_filters JSONB, ...)
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
## JSON vs JSONB for Function Parameters
|
|
76
|
+
|
|
77
|
+
### External API Parameters: Use JSON
|
|
78
|
+
|
|
79
|
+
When defining function parameters that accept JSON from external callers (API boundary), use `JSON` type (text-based) rather than `JSONB`. This allows callers to pass `JSON.stringify(options)` as a plain string without needing special serialization like `sql.json()`.
|
|
80
|
+
|
|
81
|
+
```sql
|
|
82
|
+
-- ✅ CORRECT - JSON for external input parameters
|
|
83
|
+
CREATE FUNCTION register_user(
|
|
84
|
+
p_email TEXT,
|
|
85
|
+
p_password TEXT,
|
|
86
|
+
p_options JSON DEFAULT NULL -- Accepts plain JSON string from API
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
-- ❌ WRONG - JSONB requires special serialization from clients
|
|
90
|
+
CREATE FUNCTION register_user(
|
|
91
|
+
p_email TEXT,
|
|
92
|
+
p_password TEXT,
|
|
93
|
+
p_options JSONB DEFAULT NULL -- Harder to call from JavaScript
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Internal Operations: Cast to JSONB
|
|
98
|
+
|
|
99
|
+
Inside the function, cast to JSONB if you need JSONB operators (`->`, `->>`, `-`, `||`, `?`, etc.):
|
|
100
|
+
|
|
101
|
+
```sql
|
|
102
|
+
CREATE FUNCTION register_user(p_email TEXT, p_password TEXT, p_options JSON DEFAULT NULL)
|
|
103
|
+
RETURNS JSONB AS $$
|
|
104
|
+
DECLARE
|
|
105
|
+
v_insert_data JSONB;
|
|
106
|
+
BEGIN
|
|
107
|
+
v_insert_data := jsonb_build_object('email', p_email);
|
|
108
|
+
|
|
109
|
+
IF p_options IS NOT NULL THEN
|
|
110
|
+
-- Cast to JSONB for internal operations
|
|
111
|
+
v_insert_data := (p_options::jsonb - 'id' - 'password') || v_insert_data;
|
|
112
|
+
END IF;
|
|
113
|
+
|
|
114
|
+
-- ...
|
|
115
|
+
END;
|
|
116
|
+
$$ LANGUAGE plpgsql;
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Table Columns: Use JSONB
|
|
120
|
+
|
|
121
|
+
Table columns should still use `JSONB` for efficient storage and indexing:
|
|
122
|
+
|
|
123
|
+
```sql
|
|
124
|
+
-- ✅ CORRECT - JSONB for table columns
|
|
125
|
+
CREATE TABLE users (
|
|
126
|
+
id SERIAL PRIMARY KEY,
|
|
127
|
+
metadata JSONB DEFAULT '{}' -- Efficient storage & indexing
|
|
128
|
+
);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Summary
|
|
132
|
+
|
|
133
|
+
| Context | Type | Reason |
|
|
134
|
+
|---------|------|--------|
|
|
135
|
+
| Function input parameters | `JSON` | Easy to pass from JavaScript (`JSON.stringify()`) |
|
|
136
|
+
| Internal function operations | `::jsonb` cast | Access to JSONB operators |
|
|
137
|
+
| Table columns | `JSONB` | Efficient storage and indexing |
|
|
138
|
+
|
|
139
|
+
This pattern - **JSON for input parameters, JSONB for storage** - eliminates serialization confusion at the API boundary.
|
|
140
|
+
|
|
75
141
|
## Function Categories
|
|
76
142
|
|
|
77
143
|
### Public API Functions (No underscore)
|
|
@@ -635,7 +635,8 @@ invokej dzql.lookup organisations '{"query": "acme"}'
|
|
|
635
635
|
### Real-time Events
|
|
636
636
|
- Listen using `ws.onBroadcast((method, params) => {})`
|
|
637
637
|
- Method format: `{table}:{operation}` (e.g., "venues:update")
|
|
638
|
-
- Params include: `{table, op, pk,
|
|
638
|
+
- Params include: `{table, op, pk, data, user_id, at}`
|
|
639
|
+
- `data` contains: new state for insert/update, `null` for delete
|
|
639
640
|
- Target users via notification paths or broadcast to all
|
|
640
641
|
|
|
641
642
|
### Permissions
|
|
@@ -684,8 +685,7 @@ TABLE dzql.events {
|
|
|
684
685
|
table_name TEXT NOT NULL,
|
|
685
686
|
op TEXT NOT NULL, -- 'insert' | 'update' | 'delete'
|
|
686
687
|
pk JSONB NOT NULL, -- Primary key: {id: 1}
|
|
687
|
-
|
|
688
|
-
after JSONB, -- New values (null for delete)
|
|
688
|
+
data JSONB, -- Record data (new state for insert/update, null for delete)
|
|
689
689
|
user_id INT, -- Who made the change
|
|
690
690
|
notify_users INT[], -- Who to notify (null = all)
|
|
691
691
|
at TIMESTAMPTZ DEFAULT NOW()
|
|
@@ -753,43 +753,46 @@ try {
|
|
|
753
753
|
table: 'venues', // Table name
|
|
754
754
|
op: 'insert', // Operation: 'insert' | 'update' | 'delete'
|
|
755
755
|
pk: {id: 1}, // Primary key object
|
|
756
|
-
|
|
757
|
-
id: 1,
|
|
758
|
-
name: 'Old Name',
|
|
759
|
-
address: 'Old Address'
|
|
760
|
-
},
|
|
761
|
-
after: { // New values (null for delete)
|
|
756
|
+
data: { // Record data (new state for insert/update, null for delete)
|
|
762
757
|
id: 1,
|
|
763
758
|
name: 'New Name',
|
|
764
759
|
address: 'New Address'
|
|
765
760
|
},
|
|
766
761
|
user_id: 123, // User who made the change
|
|
767
|
-
at: '2025-01-01T12:00:00Z'
|
|
768
|
-
notify_users: [1, 2, 3] // Targeted users (null = all authenticated)
|
|
762
|
+
at: '2025-01-01T12:00:00Z' // Timestamp
|
|
769
763
|
}
|
|
770
764
|
```
|
|
771
765
|
|
|
766
|
+
**Event data by operation:**
|
|
767
|
+
| Operation | `data` field contains |
|
|
768
|
+
|-----------|----------------------|
|
|
769
|
+
| `insert` | Full new record |
|
|
770
|
+
| `update` | Full updated record (new state only) |
|
|
771
|
+
| `delete` | `null` |
|
|
772
|
+
|
|
773
|
+
**Note:** The `notify_users` field is used internally for routing but is stripped from the broadcast message sent to clients.
|
|
774
|
+
|
|
772
775
|
### Using Event Data
|
|
773
776
|
|
|
774
777
|
```javascript
|
|
775
778
|
ws.onBroadcast((method, params) => {
|
|
779
|
+
const { table, op, pk, data } = params;
|
|
780
|
+
|
|
776
781
|
// For insert
|
|
777
782
|
if (method === 'venues:insert') {
|
|
778
|
-
const newRecord =
|
|
779
|
-
// params.before is null
|
|
783
|
+
const newRecord = data;
|
|
780
784
|
}
|
|
781
785
|
|
|
782
786
|
// For update
|
|
783
787
|
if (method === 'venues:update') {
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
// Compare to detect what changed
|
|
788
|
+
const updatedRecord = data;
|
|
789
|
+
// Note: only new state is available, not the previous state
|
|
787
790
|
}
|
|
788
791
|
|
|
789
792
|
// For delete
|
|
790
793
|
if (method === 'venues:delete') {
|
|
791
|
-
|
|
792
|
-
|
|
794
|
+
// data is null for delete, use pk to identify the deleted record
|
|
795
|
+
const deletedId = pk.id;
|
|
793
796
|
}
|
|
794
797
|
});
|
|
795
798
|
```
|
package/docs/reference/api.md
CHANGED
|
@@ -892,6 +892,25 @@ const result = await ws.api.register_user({
|
|
|
892
892
|
});
|
|
893
893
|
```
|
|
894
894
|
|
|
895
|
+
**With options (for extended registration):**
|
|
896
|
+
```javascript
|
|
897
|
+
const result = await ws.api.register_user({
|
|
898
|
+
email: 'user@example.com',
|
|
899
|
+
password: 'secure-password',
|
|
900
|
+
options: {
|
|
901
|
+
org_name: 'Acme Corp',
|
|
902
|
+
role: 'admin'
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
**Parameters:**
|
|
908
|
+
| Field | Type | Required | Description |
|
|
909
|
+
|-------|------|----------|-------------|
|
|
910
|
+
| `email` | string | yes | User email address |
|
|
911
|
+
| `password` | string | yes | User password |
|
|
912
|
+
| `options` | object | no | Additional JSONB data passed to `register_user()` function |
|
|
913
|
+
|
|
895
914
|
**Returns:**
|
|
896
915
|
```javascript
|
|
897
916
|
{
|
|
@@ -902,6 +921,17 @@ const result = await ws.api.register_user({
|
|
|
902
921
|
}
|
|
903
922
|
```
|
|
904
923
|
|
|
924
|
+
**Note:** The `options` parameter requires your `register_user` PostgreSQL function to accept a third parameter:
|
|
925
|
+
```sql
|
|
926
|
+
CREATE OR REPLACE FUNCTION register_user(
|
|
927
|
+
p_email TEXT,
|
|
928
|
+
p_password TEXT,
|
|
929
|
+
p_options JSONB DEFAULT NULL
|
|
930
|
+
) RETURNS JSONB AS $$
|
|
931
|
+
-- Access options: p_options->>'org_name'
|
|
932
|
+
$$;
|
|
933
|
+
```
|
|
934
|
+
|
|
905
935
|
### Login
|
|
906
936
|
|
|
907
937
|
```javascript
|
|
@@ -911,8 +941,38 @@ const result = await ws.api.login_user({
|
|
|
911
941
|
});
|
|
912
942
|
```
|
|
913
943
|
|
|
944
|
+
**With options:**
|
|
945
|
+
```javascript
|
|
946
|
+
const result = await ws.api.login_user({
|
|
947
|
+
email: 'user@example.com',
|
|
948
|
+
password: 'password',
|
|
949
|
+
options: {
|
|
950
|
+
device_id: 'abc123',
|
|
951
|
+
remember_me: true
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
**Parameters:**
|
|
957
|
+
| Field | Type | Required | Description |
|
|
958
|
+
|-------|------|----------|-------------|
|
|
959
|
+
| `email` | string | yes | User email address |
|
|
960
|
+
| `password` | string | yes | User password |
|
|
961
|
+
| `options` | object | no | Additional JSONB data passed to `login_user()` function |
|
|
962
|
+
|
|
914
963
|
**Returns:** Same as register
|
|
915
964
|
|
|
965
|
+
**Note:** The `options` parameter requires your `login_user` PostgreSQL function to accept a third parameter:
|
|
966
|
+
```sql
|
|
967
|
+
CREATE OR REPLACE FUNCTION login_user(
|
|
968
|
+
p_email TEXT,
|
|
969
|
+
p_password TEXT,
|
|
970
|
+
p_options JSONB DEFAULT NULL
|
|
971
|
+
) RETURNS JSONB AS $$
|
|
972
|
+
-- Access options: p_options->>'device_id'
|
|
973
|
+
$$;
|
|
974
|
+
```
|
|
975
|
+
|
|
916
976
|
### Logout
|
|
917
977
|
|
|
918
978
|
```javascript
|
|
@@ -980,27 +1040,34 @@ unsubscribe();
|
|
|
980
1040
|
table: 'venues',
|
|
981
1041
|
op: 'insert' | 'update' | 'delete',
|
|
982
1042
|
pk: {id: 1}, // Primary key
|
|
983
|
-
|
|
984
|
-
after: {...}, // New values (null for delete)
|
|
1043
|
+
data: {...}, // Record data (new state for insert/update, null for delete)
|
|
985
1044
|
user_id: 123, // Who made the change
|
|
986
|
-
at: '2025-01-01T...'
|
|
987
|
-
notify_users: [1, 2] // Who to notify (null = all)
|
|
1045
|
+
at: '2025-01-01T...' // Timestamp
|
|
988
1046
|
}
|
|
989
1047
|
```
|
|
990
1048
|
|
|
1049
|
+
**Event data by operation:**
|
|
1050
|
+
| Operation | `data` field contains |
|
|
1051
|
+
|-----------|----------------------|
|
|
1052
|
+
| `insert` | Full new record |
|
|
1053
|
+
| `update` | Full updated record (new state only) |
|
|
1054
|
+
| `delete` | `null` |
|
|
1055
|
+
|
|
1056
|
+
**Note:** The `notify_users` field is used internally for routing but is stripped from the broadcast message sent to clients.
|
|
1057
|
+
|
|
991
1058
|
### Event Handling Pattern
|
|
992
1059
|
|
|
993
1060
|
```javascript
|
|
994
1061
|
ws.onBroadcast((method, params) => {
|
|
995
|
-
const data = params
|
|
1062
|
+
const { table, op, pk, data } = params;
|
|
996
1063
|
|
|
997
1064
|
if (method === 'todos:insert') {
|
|
998
1065
|
state.todos.push(data);
|
|
999
1066
|
} else if (method === 'todos:update') {
|
|
1000
|
-
const idx = state.todos.findIndex(t => t.id ===
|
|
1067
|
+
const idx = state.todos.findIndex(t => t.id === pk.id);
|
|
1001
1068
|
if (idx !== -1) state.todos[idx] = data;
|
|
1002
1069
|
} else if (method === 'todos:delete') {
|
|
1003
|
-
state.todos = state.todos.filter(t => t.id !==
|
|
1070
|
+
state.todos = state.todos.filter(t => t.id !== pk.id);
|
|
1004
1071
|
}
|
|
1005
1072
|
|
|
1006
1073
|
render();
|
package/docs/reference/client.md
CHANGED
|
@@ -82,6 +82,47 @@ async function login() {
|
|
|
82
82
|
</template>
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
**Registration with options (e.g., organisation name):**
|
|
86
|
+
```vue
|
|
87
|
+
<script setup>
|
|
88
|
+
import { ref } from 'vue'
|
|
89
|
+
import { useWsStore } from 'dzql/client/stores'
|
|
90
|
+
|
|
91
|
+
const wsStore = useWsStore()
|
|
92
|
+
const email = ref('')
|
|
93
|
+
const password = ref('')
|
|
94
|
+
const orgName = ref('')
|
|
95
|
+
|
|
96
|
+
async function register() {
|
|
97
|
+
try {
|
|
98
|
+
await wsStore.register({
|
|
99
|
+
email: email.value,
|
|
100
|
+
password: password.value,
|
|
101
|
+
options: { org_name: orgName.value }
|
|
102
|
+
})
|
|
103
|
+
} catch (err) {
|
|
104
|
+
alert(err.message)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<template>
|
|
110
|
+
<form @submit.prevent="register">
|
|
111
|
+
<input v-model="email" type="email" placeholder="Email" required />
|
|
112
|
+
<input v-model="password" type="password" placeholder="Password" required />
|
|
113
|
+
<input v-model="orgName" type="text" placeholder="Organisation Name" />
|
|
114
|
+
<button type="submit">Register</button>
|
|
115
|
+
</form>
|
|
116
|
+
</template>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The `options` parameter allows passing additional JSONB data to the `register_user` and `login_user` PostgreSQL functions. This is useful for:
|
|
120
|
+
- Organisation name during registration
|
|
121
|
+
- Device ID for login tracking
|
|
122
|
+
- Any custom fields your auth functions support
|
|
123
|
+
|
|
124
|
+
See [API Reference - Authentication](./api.md#authentication) for details on configuring your PostgreSQL functions.
|
|
125
|
+
|
|
85
126
|
## 4. Setup main.js
|
|
86
127
|
|
|
87
128
|
**src/main.js:**
|
package/package.json
CHANGED
package/src/client/ws.js
CHANGED
|
@@ -23,6 +23,13 @@
|
|
|
23
23
|
* password: 'password123'
|
|
24
24
|
* });
|
|
25
25
|
*
|
|
26
|
+
* // Register with options (e.g., organisation name)
|
|
27
|
+
* const session = await ws.api.register_user({
|
|
28
|
+
* email: 'user@example.com',
|
|
29
|
+
* password: 'password123',
|
|
30
|
+
* options: { org_name: 'Acme Corp' }
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
26
33
|
* // CRUD operations
|
|
27
34
|
* const venue = await ws.api.get.venues({ id: 1 });
|
|
28
35
|
* const created = await ws.api.save.venues({ name: 'New Venue' });
|
|
@@ -28,6 +28,9 @@ export class AuthCodegen {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
return [
|
|
31
|
+
'-- Enable pgcrypto extension for password hashing',
|
|
32
|
+
'CREATE EXTENSION IF NOT EXISTS pgcrypto;',
|
|
33
|
+
'',
|
|
31
34
|
this._generateProfileFunction(),
|
|
32
35
|
this._generateRegisterFunction(),
|
|
33
36
|
this._generateLoginFunction()
|
|
@@ -57,16 +60,16 @@ $$;`;
|
|
|
57
60
|
|
|
58
61
|
/**
|
|
59
62
|
* Generate register_user function
|
|
60
|
-
* Supports optional
|
|
63
|
+
* Supports optional fields via JSON parameter
|
|
61
64
|
* @private
|
|
62
65
|
*/
|
|
63
66
|
_generateRegisterFunction() {
|
|
64
67
|
return `-- ============================================================================
|
|
65
68
|
-- Auth: register_user function for ${this.tableName}
|
|
66
|
-
--
|
|
69
|
+
-- p_options: optional JSON object with additional fields to set on the user record
|
|
67
70
|
-- Example: register_user('test@example.com', 'password', '{"name": "Test User"}')
|
|
68
71
|
-- ============================================================================
|
|
69
|
-
CREATE OR REPLACE FUNCTION register_user(p_email TEXT, p_password TEXT,
|
|
72
|
+
CREATE OR REPLACE FUNCTION register_user(p_email TEXT, p_password TEXT, p_options JSON DEFAULT NULL)
|
|
70
73
|
RETURNS JSONB
|
|
71
74
|
LANGUAGE plpgsql
|
|
72
75
|
SECURITY DEFINER
|
|
@@ -81,9 +84,12 @@ BEGIN
|
|
|
81
84
|
v_salt := gen_salt('bf', 10);
|
|
82
85
|
v_hash := crypt(p_password, v_salt);
|
|
83
86
|
|
|
84
|
-
-- Build insert data:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
-- Build insert data: options fields + email + password_hash (options cannot override core fields)
|
|
88
|
+
-- Cast p_options to JSONB for internal operations (JSON type is for API boundary convenience)
|
|
89
|
+
v_insert_data := jsonb_build_object('email', p_email, 'password_hash', v_hash);
|
|
90
|
+
IF p_options IS NOT NULL THEN
|
|
91
|
+
v_insert_data := (p_options::jsonb - 'id' - 'email' - 'password_hash' - 'password') || v_insert_data;
|
|
92
|
+
END IF;
|
|
87
93
|
|
|
88
94
|
-- Dynamic INSERT from JSONB (same pattern as compiled save functions)
|
|
89
95
|
EXECUTE (
|
|
@@ -5,12 +5,10 @@
|
|
|
5
5
|
create extension if not exists pgcrypto;
|
|
6
6
|
|
|
7
7
|
-- === Users Table ===
|
|
8
|
-
--
|
|
9
|
-
-- Applications can add additional columns as needed
|
|
8
|
+
-- Minimal auth table - applications can add columns via migrations
|
|
10
9
|
-- Note: created_at is tracked via the action log, not here
|
|
11
10
|
create table if not exists users (
|
|
12
11
|
id serial primary key,
|
|
13
|
-
name text,
|
|
14
12
|
email text unique not null,
|
|
15
13
|
password_hash text not null
|
|
16
14
|
);
|
|
@@ -18,9 +16,9 @@ create table if not exists users (
|
|
|
18
16
|
-- === Auth Functions ===
|
|
19
17
|
|
|
20
18
|
-- Register new user
|
|
21
|
-
--
|
|
19
|
+
-- p_options: optional JSON object with additional fields to set on the user record
|
|
22
20
|
-- Example: register_user('test@example.com', 'password', '{"name": "Test User"}')
|
|
23
|
-
create or replace function register_user(p_email text, p_password text,
|
|
21
|
+
create or replace function register_user(p_email text, p_password text, p_options jsonb default null)
|
|
24
22
|
returns jsonb
|
|
25
23
|
language plpgsql
|
|
26
24
|
security definer
|
|
@@ -35,9 +33,11 @@ begin
|
|
|
35
33
|
v_salt := gen_salt('bf', 10);
|
|
36
34
|
v_hash := crypt(p_password, v_salt);
|
|
37
35
|
|
|
38
|
-
-- Build insert data:
|
|
39
|
-
v_insert_data := (
|
|
40
|
-
|
|
36
|
+
-- Build insert data: options fields + email + password_hash (options cannot override core fields)
|
|
37
|
+
v_insert_data := jsonb_build_object('email', p_email, 'password_hash', v_hash);
|
|
38
|
+
if p_options is not null then
|
|
39
|
+
v_insert_data := (p_options - 'id' - 'email' - 'password_hash' - 'password') || v_insert_data;
|
|
40
|
+
end if;
|
|
41
41
|
|
|
42
42
|
-- Dynamic INSERT from JSONB (same pattern as compiled save functions)
|
|
43
43
|
execute (
|
package/src/server/db.js
CHANGED
|
@@ -54,7 +54,13 @@ export async function setCache(key, data) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// Auth helpers
|
|
57
|
-
export async function callAuthFunction(method, email, password) {
|
|
57
|
+
export async function callAuthFunction(method, email, password, options = null) {
|
|
58
|
+
if (options !== null) {
|
|
59
|
+
const result = await sql`
|
|
60
|
+
SELECT ${sql(method)}(${email}, ${password}, ${options}) as result
|
|
61
|
+
`;
|
|
62
|
+
return result[0].result;
|
|
63
|
+
}
|
|
58
64
|
const result = await sql`
|
|
59
65
|
SELECT ${sql(method)}(${email}, ${password}) as result
|
|
60
66
|
`;
|
|
@@ -380,7 +386,7 @@ export const db = {
|
|
|
380
386
|
// Special handling for auth functions that don't require userId
|
|
381
387
|
if (prop === 'register_user' || prop === 'login_user') {
|
|
382
388
|
// For auth functions, first param is the args object
|
|
383
|
-
return callAuthFunction(prop, userIdOrArgs.email, userIdOrArgs.password);
|
|
389
|
+
return callAuthFunction(prop, userIdOrArgs.email, userIdOrArgs.password, userIdOrArgs.options || null);
|
|
384
390
|
}
|
|
385
391
|
|
|
386
392
|
// For other functions, userId is required as first parameter
|
package/src/server/ws.js
CHANGED
|
@@ -231,6 +231,7 @@ export function createRPCHandler(customHandlers = {}) {
|
|
|
231
231
|
"login_user",
|
|
232
232
|
params.email,
|
|
233
233
|
params.password,
|
|
234
|
+
params.options || null,
|
|
234
235
|
);
|
|
235
236
|
|
|
236
237
|
// On successful auth, set user_id on WebSocket connection
|
|
@@ -268,6 +269,7 @@ export function createRPCHandler(customHandlers = {}) {
|
|
|
268
269
|
"register_user",
|
|
269
270
|
params.email,
|
|
270
271
|
params.password,
|
|
272
|
+
params.options || null,
|
|
271
273
|
);
|
|
272
274
|
|
|
273
275
|
// On successful registration, set user_id on WebSocket connection
|