dzql 0.6.16 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dzql",
3
- "version": "0.6.16",
3
+ "version": "0.6.18",
4
4
  "description": "Database-first real-time framework with TypeScript support",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,8 +15,27 @@ export function generateClientSDK(manifest: Manifest): string {
15
15
  const subscriptionFunctions = Object.entries(manifest.functions)
16
16
  .filter(([_, def]) => def.isSubscription);
17
17
 
18
- // Generate entity types
19
- const typeDefs = generateTypeDefinitions(manifest.entities, manifest.subscribables);
18
+ // Build a map of custom functions with typed params/returns
19
+ const typedCustomFunctions = new Map<string, { hasParams: boolean; hasReturns: boolean; returnsScalar: boolean }>();
20
+ if (manifest.customFunctions) {
21
+ for (const fn of manifest.customFunctions) {
22
+ const hasParams = fn.params && Object.keys(fn.params).length > 0;
23
+ const hasReturns = fn.returns !== undefined;
24
+ const returnsScalar = typeof fn.returns === 'string';
25
+ typedCustomFunctions.set(fn.name, { hasParams: !!hasParams, hasReturns, returnsScalar });
26
+ }
27
+ }
28
+
29
+ // Generate entity types (now with auth, subscribable results, and custom function types)
30
+ const typeDefs = generateTypeDefinitions(
31
+ manifest.entities,
32
+ manifest.subscribables,
33
+ manifest.auth,
34
+ manifest.customFunctions
35
+ );
36
+
37
+ // Check if we have auth types
38
+ const hasAuth = manifest.auth !== undefined;
20
39
 
21
40
  // Generate API interface with proper types
22
41
  const apiMethods = Object.entries(manifest.functions).map(([funcName, def]) => {
@@ -29,18 +48,34 @@ export function generateClientSDK(manifest: Manifest): string {
29
48
  // Check if this entity exists in manifest
30
49
  const entityExists = manifest.entities[entity];
31
50
 
51
+ // Check if this is a subscribable
52
+ const subscribableExists = manifest.subscribables?.[entity];
53
+
32
54
  let paramType = 'Record<string, unknown>';
33
55
  let returnType = 'unknown';
34
56
 
57
+ // Handle auth functions
58
+ if (funcName === 'login_user' && hasAuth) {
59
+ return ` ${funcName}: (params: LoginParams) => Promise<LoginResult>;`;
60
+ }
61
+ if (funcName === 'register_user' && hasAuth) {
62
+ return ` ${funcName}: (params: RegisterParams) => Promise<RegisterResult>;`;
63
+ }
64
+
65
+ // Handle subscriptions
35
66
  if (isSubscription) {
36
- // subscribe_venue_detail -> VenueDetailParams
67
+ // subscribe_venue_detail -> VenueDetailParams, VenueDetailResult
37
68
  paramType = `${pascalEntity}Params`;
38
- return ` ${funcName}: (params: ${paramType}, callback: (data: unknown) => void) => Promise<{ data: unknown; subscription_id: string; schema: unknown; unsubscribe: () => Promise<void> }>;`;
39
- } else if (op === 'get' && entityExists) {
69
+ const resultType = subscribableExists ? `${pascalEntity}Result` : 'unknown';
70
+ return ` ${funcName}: (params: ${paramType}, callback: (data: ${resultType}) => void) => Promise<{ data: ${resultType}; subscription_id: string; schema: unknown; unsubscribe: () => Promise<void> }>;`;
71
+ }
72
+
73
+ // Handle entity CRUD operations
74
+ if (op === 'get' && entityExists) {
40
75
  // get_venue_detail (subscribable getter) vs get_venues (entity getter)
41
- if (manifest.subscribables?.[entity]) {
76
+ if (subscribableExists) {
42
77
  paramType = `${pascalEntity}Params`;
43
- returnType = 'unknown';
78
+ returnType = `${pascalEntity}Result`;
44
79
  } else {
45
80
  paramType = `${pascalEntity}PK`;
46
81
  returnType = `${pascalEntity} | null`;
@@ -58,7 +93,24 @@ export function generateClientSDK(manifest: Manifest): string {
58
93
  paramType = `Lookup${pascalEntity}Params`;
59
94
  returnType = `${pascalEntity}[]`;
60
95
  }
61
- // Custom functions and auth functions stay as Record<string, unknown>
96
+
97
+ // Handle custom functions with typed params/returns
98
+ const customFnInfo = typedCustomFunctions.get(funcName);
99
+ if (customFnInfo) {
100
+ const pascalFuncName = toPascalCase(funcName);
101
+ if (customFnInfo.hasParams) {
102
+ paramType = `${pascalFuncName}Params`;
103
+ }
104
+ if (customFnInfo.hasReturns) {
105
+ if (customFnInfo.returnsScalar) {
106
+ // Find the scalar type from the manifest
107
+ const fn = manifest.customFunctions?.find(f => f.name === funcName);
108
+ returnType = fn?.returns as string || 'unknown';
109
+ } else {
110
+ returnType = `${pascalFuncName}Result`;
111
+ }
112
+ }
113
+ }
62
114
 
63
115
  return ` ${funcName}: (params: ${paramType}) => Promise<${returnType}>;`;
64
116
  }).join('\n');
@@ -1,10 +1,12 @@
1
- import { DomainIR, EntityIR, SubscribableIR } from "../../shared/ir.js";
1
+ import { DomainIR, EntityIR, SubscribableIR, AuthIR, CustomFunctionIR } from "../../shared/ir.js";
2
2
 
3
3
  export interface Manifest {
4
4
  version: string;
5
5
  functions: Record<string, FunctionDef>;
6
6
  entities: Record<string, EntityIR>;
7
7
  subscribables: Record<string, SubscribableIR>;
8
+ auth?: AuthIR;
9
+ customFunctions?: CustomFunctionIR[];
8
10
  }
9
11
 
10
12
  export interface FunctionDef {
@@ -18,9 +20,9 @@ export interface FunctionDef {
18
20
 
19
21
  export function generateManifest(ir: DomainIR): Manifest {
20
22
  const functions: Record<string, FunctionDef> = {
21
- // Built-in Auth Functions
22
- login_user: { schema: 'dzql_v2', name: 'login_user', args: ['p_params'], returnType: 'jsonb' },
23
- register_user: { schema: 'dzql_v2', name: 'register_user', args: ['p_params'], returnType: 'jsonb' }
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' }
24
26
  };
25
27
 
26
28
  for (const [name, entity] of Object.entries(ir.entities)) {
@@ -90,6 +92,8 @@ export function generateManifest(ir: DomainIR): Manifest {
90
92
  version: "2.0.0",
91
93
  functions,
92
94
  entities: ir.entities, // Pass through for client generator
93
- subscribables: ir.subscribables
95
+ subscribables: ir.subscribables,
96
+ auth: ir.auth,
97
+ customFunctions: ir.customFunctions
94
98
  };
95
99
  }
@@ -71,11 +71,40 @@ BEGIN
71
71
  RETURN ARRAY[]::text[];
72
72
  END;
73
73
  $$;
74
+ `;
75
+ }
74
76
 
75
- -- === AUTH FUNCTIONS ===
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 User
78
- CREATE OR REPLACE FUNCTION dzql_v2.register_user(p_params jsonb)
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
- v_email text;
87
- v_password text;
88
- v_name text;
89
- v_options jsonb;
115
+ v_salt text;
116
+ v_hash text;
117
+ v_insert_data jsonb;
90
118
  BEGIN
91
- v_email := p_params->>'email';
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
- INSERT INTO users (email, password_hash, name)
101
- VALUES (v_email, crypt(v_password, gen_salt('bf')), v_name)
102
- RETURNING id INTO v_user_id;
123
+ -- Generate salt and hash password
124
+ v_salt := gen_salt('bf', 10);
125
+ v_hash := crypt(p_password, v_salt);
103
126
 
104
- -- TODO: Handle v_options if needed (e.g. creating orgs)
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
- -- Return minimal profile (Token generation happens in Runtime layer)
107
- RETURN jsonb_build_object(
108
- 'user_id', v_user_id,
109
- 'email', v_email,
110
- 'name', v_name
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 User
116
- CREATE OR REPLACE FUNCTION dzql_v2.login_user(p_params jsonb)
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 * INTO v_user FROM users WHERE email = p_params->>'email';
163
+ SELECT id, email, password_hash
164
+ INTO v_user
165
+ FROM users
166
+ WHERE email = p_email;
126
167
 
127
- IF v_user IS NULL OR v_user.password_hash != crypt(p_params->>'password', v_user.password_hash) THEN
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
- RETURN jsonb_build_object(
132
- 'user_id', v_user.id,
133
- 'email', v_user.email,
134
- 'name', v_user.name
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
  `;
@@ -1,4 +1,4 @@
1
- import { EntityIR, SubscribableIR } from "../../shared/ir.js";
1
+ import { EntityIR, SubscribableIR, AuthIR, CustomFunctionIR } from "../../shared/ir.js";
2
2
 
3
3
  const TYPE_MAP: Record<string, string> = {
4
4
  'text': 'string',
@@ -19,14 +19,18 @@ const PARAM_TYPE_MAP: Record<string, string> = {
19
19
  'integer': 'number',
20
20
  'text': 'string',
21
21
  'string': 'string',
22
+ 'number': 'number',
22
23
  'boolean': 'boolean',
23
24
  'date': 'string',
24
- 'timestamptz': 'string'
25
+ 'timestamptz': 'string',
26
+ 'object': 'Record<string, unknown>'
25
27
  };
26
28
 
27
29
  export function generateTypeDefinitions(
28
30
  entities: Record<string, EntityIR>,
29
- subscribables?: Record<string, SubscribableIR>
31
+ subscribables?: Record<string, SubscribableIR>,
32
+ auth?: AuthIR,
33
+ customFunctions?: CustomFunctionIR[]
30
34
  ): string {
31
35
  let output = "";
32
36
 
@@ -49,6 +53,7 @@ export type FilterValue<T> = T | FilterOperators<T>;
49
53
 
50
54
  `;
51
55
 
56
+ // --- Entity Types ---
52
57
  for (const [name, entity] of Object.entries(entities)) {
53
58
  const pascalName = toPascalCase(name);
54
59
 
@@ -106,17 +111,109 @@ export type FilterValue<T> = T | FilterOperators<T>;
106
111
  }\n\n`;
107
112
  }
108
113
 
109
- // --- Subscribable Params ---
114
+ // --- Subscribable Params and Result Types ---
110
115
  if (subscribables) {
111
116
  for (const [name, sub] of Object.entries(subscribables)) {
112
117
  const pascalName = toPascalCase(name);
113
118
 
119
+ // Params interface
114
120
  output += `export interface ${pascalName}Params {\n`;
115
121
  for (const [paramName, paramType] of Object.entries(sub.params)) {
116
122
  const tsType = PARAM_TYPE_MAP[paramType as string] || 'any';
117
123
  output += ` ${paramName}: ${tsType};\n`;
118
124
  }
119
125
  output += `}\n\n`;
126
+
127
+ // Result interface (extends root entity with includes)
128
+ const rootEntity = sub.root?.entity;
129
+ const rootEntityPascal = rootEntity ? toPascalCase(rootEntity) : null;
130
+
131
+ if (rootEntityPascal && entities[rootEntity]) {
132
+ output += `export interface ${pascalName}Result extends ${rootEntityPascal} {\n`;
133
+
134
+ // Add includes as optional fields
135
+ if (sub.includes) {
136
+ for (const [includeKey, includeIR] of Object.entries(sub.includes)) {
137
+ const includeEntity = includeIR.entity;
138
+ const includeEntityPascal = toPascalCase(includeEntity);
139
+
140
+ // Determine if it's an array (one-to-many) or single (many-to-one)
141
+ // For now, assume arrays unless the include key matches a FK pattern
142
+ const isArray = !includeKey.endsWith('_id') && includeKey !== rootEntity;
143
+ const includeType = isArray ? `${includeEntityPascal}[]` : includeEntityPascal;
144
+
145
+ output += ` ${includeKey}?: ${includeType};\n`;
146
+ }
147
+ }
148
+
149
+ output += `}\n\n`;
150
+ }
151
+ }
152
+ }
153
+
154
+ // --- Auth Types ---
155
+ if (auth) {
156
+ // AuthUser interface
157
+ output += `export interface AuthUser {\n`;
158
+ for (const [fieldName, fieldType] of Object.entries(auth.userFields)) {
159
+ const tsType = PARAM_TYPE_MAP[fieldType] || fieldType;
160
+ output += ` ${fieldName}: ${tsType};\n`;
161
+ }
162
+ output += `}\n\n`;
163
+
164
+ // LoginParams interface
165
+ output += `export interface LoginParams {\n`;
166
+ for (const [paramName, paramType] of Object.entries(auth.loginParams)) {
167
+ const tsType = PARAM_TYPE_MAP[paramType] || paramType;
168
+ output += ` ${paramName}: ${tsType};\n`;
169
+ }
170
+ output += `}\n\n`;
171
+
172
+ // LoginResult interface
173
+ output += `export interface LoginResult extends AuthUser {\n`;
174
+ output += ` token: string;\n`;
175
+ output += `}\n\n`;
176
+
177
+ // RegisterParams interface
178
+ output += `export interface RegisterParams {\n`;
179
+ for (const [paramName, paramType] of Object.entries(auth.registerParams)) {
180
+ const tsType = PARAM_TYPE_MAP[paramType] || paramType;
181
+ // options is optional (p_options jsonb DEFAULT NULL)
182
+ const optional = paramName === 'options' ? '?' : '';
183
+ output += ` ${paramName}${optional}: ${tsType};\n`;
184
+ }
185
+ output += `}\n\n`;
186
+
187
+ // RegisterResult interface
188
+ output += `export interface RegisterResult extends AuthUser {\n`;
189
+ output += ` token: string;\n`;
190
+ output += `}\n\n`;
191
+ }
192
+
193
+ // --- Custom Function Types ---
194
+ if (customFunctions) {
195
+ for (const fn of customFunctions) {
196
+ const pascalName = toPascalCase(fn.name);
197
+
198
+ // Generate params interface if params are defined
199
+ if (fn.params && Object.keys(fn.params).length > 0) {
200
+ output += `export interface ${pascalName}Params {\n`;
201
+ for (const [paramName, paramType] of Object.entries(fn.params)) {
202
+ const tsType = PARAM_TYPE_MAP[paramType] || paramType;
203
+ output += ` ${paramName}: ${tsType};\n`;
204
+ }
205
+ output += `}\n\n`;
206
+ }
207
+
208
+ // Generate result interface if returns is an object
209
+ if (fn.returns && typeof fn.returns === 'object') {
210
+ output += `export interface ${pascalName}Result {\n`;
211
+ for (const [fieldName, fieldType] of Object.entries(fn.returns)) {
212
+ const tsType = PARAM_TYPE_MAP[fieldType] || fieldType;
213
+ output += ` ${fieldName}: ${tsType};\n`;
214
+ }
215
+ output += `}\n\n`;
216
+ }
120
217
  }
121
218
  }
122
219
 
@@ -12,7 +12,8 @@ import type {
12
12
  IncludeIR,
13
13
  CustomFunctionIR,
14
14
  ManyToManyIR,
15
- GraphRuleIR
15
+ GraphRuleIR,
16
+ AuthIR
16
17
  } from "../../shared/ir.js";
17
18
 
18
19
  /**
@@ -224,14 +225,68 @@ export function generateIR(domain: DomainConfig): DomainIR {
224
225
  customFunctions.push({
225
226
  name: fn.name,
226
227
  sql: fn.sql,
227
- args: fn.args || ['p_user_id', 'p_params']
228
+ args: fn.args || ['p_user_id', 'p_params'],
229
+ params: fn.params,
230
+ returns: fn.returns
228
231
  });
229
232
  }
230
233
  }
231
234
 
235
+ // --- AUTH CONFIG ---
236
+ // Generate default auth config based on users entity if not provided
237
+ let auth: AuthIR | undefined;
238
+
239
+ if (domain.auth) {
240
+ // Use explicit auth config
241
+ auth = {
242
+ userFields: domain.auth.userFields || {},
243
+ loginParams: domain.auth.loginParams || { email: 'string', password: 'string' },
244
+ registerParams: domain.auth.registerParams || { email: 'string', password: 'string', options: 'object' }
245
+ };
246
+ } else if (entities['users']) {
247
+ // Derive auth config from users entity
248
+ const usersEntity = entities['users'];
249
+ const hiddenFields = new Set(usersEntity.hidden || []);
250
+ hiddenFields.add('password_hash');
251
+ hiddenFields.add('password');
252
+
253
+ // Map user columns to TypeScript types (excluding hidden fields)
254
+ const userFields: Record<string, string> = {
255
+ user_id: 'number' // Always include user_id
256
+ };
257
+
258
+ for (const col of usersEntity.columns) {
259
+ if (!hiddenFields.has(col.name) && col.name !== 'id') {
260
+ userFields[col.name] = mapPgTypeToTsType(col.type);
261
+ }
262
+ }
263
+
264
+ auth = {
265
+ userFields,
266
+ loginParams: { email: 'string', password: 'string' },
267
+ registerParams: { email: 'string', password: 'string', options: 'object' }
268
+ };
269
+ }
270
+
232
271
  return {
233
272
  entities,
234
273
  subscribables,
235
- customFunctions
274
+ customFunctions,
275
+ auth
236
276
  };
237
277
  }
278
+
279
+ /**
280
+ * Maps PostgreSQL column type to TypeScript type string
281
+ */
282
+ function mapPgTypeToTsType(pgType: string): string {
283
+ const lower = pgType.toLowerCase();
284
+ if (lower.includes('int') || lower.includes('serial') || lower.includes('decimal') || lower.includes('numeric')) {
285
+ return 'number';
286
+ }
287
+ if (lower.includes('bool')) {
288
+ return 'boolean';
289
+ }
290
+ // Default to string for text, timestamps, etc.
291
+ return 'string';
292
+ }
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
- const schemaContent = customFunctionSQL.length > 0
112
- ? entitySQL.join('\n') + '\n\n-- Custom Functions\n' + customFunctionSQL.join('\n\n')
113
- : entitySQL.join('\n');
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) {
@@ -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: ["p_params"] -> [$2] (since $2 is the data param)
57
- // We map: p_user_id -> userId ($1), p_data/p_pk/p_params -> params ($2)
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);
package/src/shared/ir.ts CHANGED
@@ -100,6 +100,20 @@ export interface CustomFunctionConfig {
100
100
  name: string;
101
101
  sql: string;
102
102
  args?: string[];
103
+ /** Parameter types for TypeScript generation: { paramName: 'number' | 'string' | ... } */
104
+ params?: Record<string, string>;
105
+ /** Return type: either an object shape or a scalar type name */
106
+ returns?: Record<string, string> | string;
107
+ }
108
+
109
+ /** Auth configuration for TypeScript type generation */
110
+ export interface AuthConfig {
111
+ /** Fields returned by auth functions (user profile shape) */
112
+ userFields?: Record<string, string>;
113
+ /** Login function parameter types */
114
+ loginParams?: Record<string, string>;
115
+ /** Register function parameter types */
116
+ registerParams?: Record<string, string>;
103
117
  }
104
118
 
105
119
  /** Complete domain configuration as provided in domain file */
@@ -107,6 +121,7 @@ export interface DomainConfig {
107
121
  entities: Record<string, EntityConfig>;
108
122
  subscribables?: Record<string, SubscribableConfig>;
109
123
  customFunctions?: CustomFunctionConfig[];
124
+ auth?: AuthConfig;
110
125
  }
111
126
 
112
127
  // ============================================
@@ -191,10 +206,25 @@ export interface CustomFunctionIR {
191
206
  name: string;
192
207
  sql: string;
193
208
  args?: string[]; // For manifest allowlist
209
+ /** Parameter types for TypeScript generation */
210
+ params?: Record<string, string>;
211
+ /** Return type: either an object shape or a scalar type name */
212
+ returns?: Record<string, string> | string;
213
+ }
214
+
215
+ /** Auth IR for TypeScript type generation */
216
+ export interface AuthIR {
217
+ /** Fields returned by auth functions (user profile shape) */
218
+ userFields: Record<string, string>;
219
+ /** Login function parameter types */
220
+ loginParams: Record<string, string>;
221
+ /** Register function parameter types */
222
+ registerParams: Record<string, string>;
194
223
  }
195
224
 
196
225
  export interface DomainIR {
197
226
  entities: Record<string, EntityIR>;
198
227
  subscribables: Record<string, SubscribableIR>;
199
228
  customFunctions: CustomFunctionIR[];
229
+ auth?: AuthIR;
200
230
  }