dzql 0.6.17 → 0.6.18
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 +1 -1
- package/src/cli/codegen/manifest.ts +3 -3
- package/src/cli/codegen/sql.ts +74 -33
- package/src/cli/codegen/types.ts +5 -2
- package/src/cli/compiler/ir.ts +2 -2
- package/src/cli/index.ts +9 -5
- package/src/runtime/server.ts +10 -2
package/package.json
CHANGED
|
@@ -20,9 +20,9 @@ export interface FunctionDef {
|
|
|
20
20
|
|
|
21
21
|
export function generateManifest(ir: DomainIR): Manifest {
|
|
22
22
|
const functions: Record<string, FunctionDef> = {
|
|
23
|
-
// Built-in Auth Functions
|
|
24
|
-
login_user: { schema: 'dzql_v2', name: 'login_user', args: ['
|
|
25
|
-
register_user: { schema: 'dzql_v2', name: 'register_user', args: ['
|
|
23
|
+
// Built-in Auth Functions (individual params, not jsonb)
|
|
24
|
+
login_user: { schema: 'dzql_v2', name: 'login_user', args: ['p_email', 'p_password'], returnType: 'jsonb' },
|
|
25
|
+
register_user: { schema: 'dzql_v2', name: 'register_user', args: ['p_email', 'p_password', 'p_options'], returnType: 'jsonb' }
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
for (const [name, entity] of Object.entries(ir.entities)) {
|
package/src/cli/codegen/sql.ts
CHANGED
|
@@ -71,11 +71,40 @@ BEGIN
|
|
|
71
71
|
RETURN ARRAY[]::text[];
|
|
72
72
|
END;
|
|
73
73
|
$$;
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Generate auth SQL functions.
|
|
79
|
+
* These depend on the 'users' table existing, so must be applied after schema.
|
|
80
|
+
*/
|
|
81
|
+
export function generateAuthSQL() {
|
|
82
|
+
return `
|
|
83
|
+
-- === AUTH SYSTEM ===
|
|
84
|
+
-- These functions depend on a 'users' table being created by the domain schema.
|
|
85
|
+
-- The users table must have: id (serial PK), email (text unique), password_hash (text)
|
|
86
|
+
-- Additional columns can be added and will be returned by _profile() automatically.
|
|
87
|
+
|
|
88
|
+
-- Get user profile (private function)
|
|
89
|
+
-- Returns all user columns except sensitive fields (password_hash, password, secret, token)
|
|
90
|
+
-- This allows the users table to have any additional columns without modifying this function
|
|
91
|
+
CREATE OR REPLACE FUNCTION dzql_v2._profile(p_user_id int)
|
|
92
|
+
RETURNS jsonb
|
|
93
|
+
LANGUAGE sql
|
|
94
|
+
SECURITY DEFINER
|
|
95
|
+
SET search_path = dzql_v2, public
|
|
96
|
+
AS $$
|
|
97
|
+
SELECT jsonb_build_object('user_id', u.id) || (to_jsonb(u.*) - 'id' - 'password_hash' - 'password' - 'secret' - 'token')
|
|
98
|
+
FROM users u
|
|
99
|
+
WHERE id = p_user_id;
|
|
100
|
+
$$;
|
|
76
101
|
|
|
77
|
-
-- Register
|
|
78
|
-
|
|
102
|
+
-- Register new user
|
|
103
|
+
-- p_email: User's email address (must be unique)
|
|
104
|
+
-- p_password: User's password (will be hashed)
|
|
105
|
+
-- p_options: Optional JSON object with additional fields to set on the user record
|
|
106
|
+
-- Example: register_user('test@example.com', 'password', '{"name": "Test User"}')
|
|
107
|
+
CREATE OR REPLACE FUNCTION dzql_v2.register_user(p_email text, p_password text, p_options jsonb DEFAULT NULL)
|
|
79
108
|
RETURNS jsonb
|
|
80
109
|
LANGUAGE plpgsql
|
|
81
110
|
SECURITY DEFINER
|
|
@@ -83,37 +112,46 @@ SET search_path = dzql_v2, public
|
|
|
83
112
|
AS $$
|
|
84
113
|
DECLARE
|
|
85
114
|
v_user_id int;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
v_options jsonb;
|
|
115
|
+
v_salt text;
|
|
116
|
+
v_hash text;
|
|
117
|
+
v_insert_data jsonb;
|
|
90
118
|
BEGIN
|
|
91
|
-
|
|
92
|
-
v_password := p_params->>'password';
|
|
93
|
-
v_name := COALESCE(p_params->>'name', v_email);
|
|
94
|
-
v_options := COALESCE(p_params->'options', '{}'::jsonb);
|
|
95
|
-
|
|
96
|
-
IF v_email IS NULL OR v_password IS NULL THEN
|
|
119
|
+
IF p_email IS NULL OR p_password IS NULL THEN
|
|
97
120
|
RAISE EXCEPTION 'validation_error: email and password required';
|
|
98
121
|
END IF;
|
|
99
122
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
123
|
+
-- Generate salt and hash password
|
|
124
|
+
v_salt := gen_salt('bf', 10);
|
|
125
|
+
v_hash := crypt(p_password, v_salt);
|
|
103
126
|
|
|
104
|
-
--
|
|
127
|
+
-- Build insert data: options fields + email + password_hash (options cannot override core fields)
|
|
128
|
+
v_insert_data := jsonb_build_object('email', p_email, 'password_hash', v_hash);
|
|
129
|
+
IF p_options IS NOT NULL THEN
|
|
130
|
+
v_insert_data := (p_options - 'id' - 'email' - 'password_hash' - 'password') || v_insert_data;
|
|
131
|
+
END IF;
|
|
105
132
|
|
|
106
|
-
--
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
133
|
+
-- Dynamic INSERT from JSONB (same pattern as compiled save functions)
|
|
134
|
+
EXECUTE (
|
|
135
|
+
SELECT format(
|
|
136
|
+
'INSERT INTO users (%s) VALUES (%s) RETURNING id',
|
|
137
|
+
string_agg(quote_ident(key), ', '),
|
|
138
|
+
string_agg(quote_nullable(value), ', ')
|
|
139
|
+
)
|
|
140
|
+
FROM jsonb_each_text(v_insert_data) kv(key, value)
|
|
141
|
+
) INTO v_user_id;
|
|
142
|
+
|
|
143
|
+
-- Return profile (Token generation happens in Runtime layer)
|
|
144
|
+
RETURN dzql_v2._profile(v_user_id);
|
|
145
|
+
EXCEPTION
|
|
146
|
+
WHEN unique_violation THEN
|
|
147
|
+
RAISE EXCEPTION 'validation_error: Email already exists' USING ERRCODE = '23505';
|
|
112
148
|
END;
|
|
113
149
|
$$;
|
|
114
150
|
|
|
115
|
-
-- Login
|
|
116
|
-
|
|
151
|
+
-- Login user
|
|
152
|
+
-- p_email: User's email address
|
|
153
|
+
-- p_password: User's password
|
|
154
|
+
CREATE OR REPLACE FUNCTION dzql_v2.login_user(p_email text, p_password text)
|
|
117
155
|
RETURNS jsonb
|
|
118
156
|
LANGUAGE plpgsql
|
|
119
157
|
SECURITY DEFINER
|
|
@@ -122,17 +160,20 @@ AS $$
|
|
|
122
160
|
DECLARE
|
|
123
161
|
v_user record;
|
|
124
162
|
BEGIN
|
|
125
|
-
SELECT
|
|
163
|
+
SELECT id, email, password_hash
|
|
164
|
+
INTO v_user
|
|
165
|
+
FROM users
|
|
166
|
+
WHERE email = p_email;
|
|
126
167
|
|
|
127
|
-
IF
|
|
128
|
-
RAISE EXCEPTION 'permission_denied: invalid credentials';
|
|
168
|
+
IF NOT FOUND THEN
|
|
169
|
+
RAISE EXCEPTION 'permission_denied: invalid credentials' USING ERRCODE = '28000';
|
|
129
170
|
END IF;
|
|
130
171
|
|
|
131
|
-
|
|
132
|
-
'
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
);
|
|
172
|
+
IF NOT (v_user.password_hash = crypt(p_password, v_user.password_hash)) THEN
|
|
173
|
+
RAISE EXCEPTION 'permission_denied: invalid credentials' USING ERRCODE = '28000';
|
|
174
|
+
END IF;
|
|
175
|
+
|
|
176
|
+
RETURN dzql_v2._profile(v_user.id);
|
|
136
177
|
END;
|
|
137
178
|
$$;
|
|
138
179
|
`;
|
package/src/cli/codegen/types.ts
CHANGED
|
@@ -22,7 +22,8 @@ const PARAM_TYPE_MAP: Record<string, string> = {
|
|
|
22
22
|
'number': 'number',
|
|
23
23
|
'boolean': 'boolean',
|
|
24
24
|
'date': 'string',
|
|
25
|
-
'timestamptz': 'string'
|
|
25
|
+
'timestamptz': 'string',
|
|
26
|
+
'object': 'Record<string, unknown>'
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
export function generateTypeDefinitions(
|
|
@@ -177,7 +178,9 @@ export type FilterValue<T> = T | FilterOperators<T>;
|
|
|
177
178
|
output += `export interface RegisterParams {\n`;
|
|
178
179
|
for (const [paramName, paramType] of Object.entries(auth.registerParams)) {
|
|
179
180
|
const tsType = PARAM_TYPE_MAP[paramType] || paramType;
|
|
180
|
-
|
|
181
|
+
// options is optional (p_options jsonb DEFAULT NULL)
|
|
182
|
+
const optional = paramName === 'options' ? '?' : '';
|
|
183
|
+
output += ` ${paramName}${optional}: ${tsType};\n`;
|
|
181
184
|
}
|
|
182
185
|
output += `}\n\n`;
|
|
183
186
|
|
package/src/cli/compiler/ir.ts
CHANGED
|
@@ -241,7 +241,7 @@ export function generateIR(domain: DomainConfig): DomainIR {
|
|
|
241
241
|
auth = {
|
|
242
242
|
userFields: domain.auth.userFields || {},
|
|
243
243
|
loginParams: domain.auth.loginParams || { email: 'string', password: 'string' },
|
|
244
|
-
registerParams: domain.auth.registerParams || { email: 'string', password: 'string' }
|
|
244
|
+
registerParams: domain.auth.registerParams || { email: 'string', password: 'string', options: 'object' }
|
|
245
245
|
};
|
|
246
246
|
} else if (entities['users']) {
|
|
247
247
|
// Derive auth config from users entity
|
|
@@ -264,7 +264,7 @@ export function generateIR(domain: DomainConfig): DomainIR {
|
|
|
264
264
|
auth = {
|
|
265
265
|
userFields,
|
|
266
266
|
loginParams: { email: 'string', password: 'string' },
|
|
267
|
-
registerParams: { email: 'string', password: 'string' }
|
|
267
|
+
registerParams: { email: 'string', password: 'string', options: 'object' }
|
|
268
268
|
};
|
|
269
269
|
}
|
|
270
270
|
|
package/src/cli/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { loadDomain } from "./compiler/loader.js";
|
|
3
3
|
import { analyzeDomain } from "./compiler/analyzer.js";
|
|
4
4
|
import { generateIR } from "./compiler/ir.js";
|
|
5
|
-
import { generateCoreSQL, generateEntitySQL, generateSchemaSQL } from "./codegen/sql.js";
|
|
5
|
+
import { generateCoreSQL, generateAuthSQL, generateEntitySQL, generateSchemaSQL } from "./codegen/sql.js";
|
|
6
6
|
import { generateSubscribableSQL, generateComputeAffectedKeysFunction } from "./codegen/subscribable_sql.js";
|
|
7
7
|
import { generateManifest } from "./codegen/manifest.js";
|
|
8
8
|
import { generateSubscribableStore } from "./codegen/subscribable_store.js";
|
|
@@ -107,10 +107,14 @@ async function main() {
|
|
|
107
107
|
writeFileSync(resolve(dbDir, `000_core.sql`), coreSQL);
|
|
108
108
|
const timestamp = new Date().toISOString().replace(/[:.-]/g, '');
|
|
109
109
|
|
|
110
|
-
// Combine entity SQL with custom functions
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
// Combine entity SQL with auth functions and custom functions
|
|
111
|
+
// Auth functions must come after schema (they depend on users table)
|
|
112
|
+
const authSQL = generateAuthSQL();
|
|
113
|
+
let schemaContent = entitySQL.join('\n');
|
|
114
|
+
schemaContent += '\n\n-- Auth Functions\n' + authSQL;
|
|
115
|
+
if (customFunctionSQL.length > 0) {
|
|
116
|
+
schemaContent += '\n\n-- Custom Functions\n' + customFunctionSQL.join('\n\n');
|
|
117
|
+
}
|
|
114
118
|
writeFileSync(resolve(dbDir, `${timestamp}_schema.sql`), schemaContent);
|
|
115
119
|
|
|
116
120
|
if (customFunctionSQL.length > 0) {
|
package/src/runtime/server.ts
CHANGED
|
@@ -53,8 +53,8 @@ export async function handleRequest(
|
|
|
53
53
|
try {
|
|
54
54
|
// Construct params array based on manifest definition
|
|
55
55
|
// args: ["p_user_id", "p_data"] -> [$1, $2]
|
|
56
|
-
// args: ["
|
|
57
|
-
// We map: p_user_id -> userId
|
|
56
|
+
// args: ["p_email", "p_password"] -> extract from params object
|
|
57
|
+
// We map: p_user_id -> userId, p_data/p_pk/p_params -> params (jsonb), individual args -> params[key]
|
|
58
58
|
|
|
59
59
|
const dbParams = [];
|
|
60
60
|
const sqlArgs = [];
|
|
@@ -66,8 +66,16 @@ export async function handleRequest(
|
|
|
66
66
|
dbParams.push(userId);
|
|
67
67
|
sqlArgs.push(`$${dbParams.length}`);
|
|
68
68
|
} else if (arg === 'p_data' || arg === 'p_pk' || arg === 'p_query' || arg === 'p_params') {
|
|
69
|
+
// Pass entire params object as jsonb
|
|
69
70
|
dbParams.push(params);
|
|
70
71
|
sqlArgs.push(`$${dbParams.length}`);
|
|
72
|
+
} else if (arg.startsWith('p_')) {
|
|
73
|
+
// Individual param: extract from params object by field name
|
|
74
|
+
// e.g., p_email -> params.email, p_password -> params.password, p_options -> params.options
|
|
75
|
+
const fieldName = arg.slice(2); // Remove 'p_' prefix
|
|
76
|
+
const value = params?.[fieldName];
|
|
77
|
+
dbParams.push(value !== undefined ? value : null);
|
|
78
|
+
sqlArgs.push(`$${dbParams.length}`);
|
|
71
79
|
} else {
|
|
72
80
|
// Unknown arg? Pass null
|
|
73
81
|
dbParams.push(null);
|