dzql 0.6.16 → 0.6.17

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.17",
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 {
@@ -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
  }
@@ -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,6 +19,7 @@ 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
25
  'timestamptz': 'string'
@@ -26,7 +27,9 @@ const PARAM_TYPE_MAP: Record<string, string> = {
26
27
 
27
28
  export function generateTypeDefinitions(
28
29
  entities: Record<string, EntityIR>,
29
- subscribables?: Record<string, SubscribableIR>
30
+ subscribables?: Record<string, SubscribableIR>,
31
+ auth?: AuthIR,
32
+ customFunctions?: CustomFunctionIR[]
30
33
  ): string {
31
34
  let output = "";
32
35
 
@@ -49,6 +52,7 @@ export type FilterValue<T> = T | FilterOperators<T>;
49
52
 
50
53
  `;
51
54
 
55
+ // --- Entity Types ---
52
56
  for (const [name, entity] of Object.entries(entities)) {
53
57
  const pascalName = toPascalCase(name);
54
58
 
@@ -106,17 +110,107 @@ export type FilterValue<T> = T | FilterOperators<T>;
106
110
  }\n\n`;
107
111
  }
108
112
 
109
- // --- Subscribable Params ---
113
+ // --- Subscribable Params and Result Types ---
110
114
  if (subscribables) {
111
115
  for (const [name, sub] of Object.entries(subscribables)) {
112
116
  const pascalName = toPascalCase(name);
113
117
 
118
+ // Params interface
114
119
  output += `export interface ${pascalName}Params {\n`;
115
120
  for (const [paramName, paramType] of Object.entries(sub.params)) {
116
121
  const tsType = PARAM_TYPE_MAP[paramType as string] || 'any';
117
122
  output += ` ${paramName}: ${tsType};\n`;
118
123
  }
119
124
  output += `}\n\n`;
125
+
126
+ // Result interface (extends root entity with includes)
127
+ const rootEntity = sub.root?.entity;
128
+ const rootEntityPascal = rootEntity ? toPascalCase(rootEntity) : null;
129
+
130
+ if (rootEntityPascal && entities[rootEntity]) {
131
+ output += `export interface ${pascalName}Result extends ${rootEntityPascal} {\n`;
132
+
133
+ // Add includes as optional fields
134
+ if (sub.includes) {
135
+ for (const [includeKey, includeIR] of Object.entries(sub.includes)) {
136
+ const includeEntity = includeIR.entity;
137
+ const includeEntityPascal = toPascalCase(includeEntity);
138
+
139
+ // Determine if it's an array (one-to-many) or single (many-to-one)
140
+ // For now, assume arrays unless the include key matches a FK pattern
141
+ const isArray = !includeKey.endsWith('_id') && includeKey !== rootEntity;
142
+ const includeType = isArray ? `${includeEntityPascal}[]` : includeEntityPascal;
143
+
144
+ output += ` ${includeKey}?: ${includeType};\n`;
145
+ }
146
+ }
147
+
148
+ output += `}\n\n`;
149
+ }
150
+ }
151
+ }
152
+
153
+ // --- Auth Types ---
154
+ if (auth) {
155
+ // AuthUser interface
156
+ output += `export interface AuthUser {\n`;
157
+ for (const [fieldName, fieldType] of Object.entries(auth.userFields)) {
158
+ const tsType = PARAM_TYPE_MAP[fieldType] || fieldType;
159
+ output += ` ${fieldName}: ${tsType};\n`;
160
+ }
161
+ output += `}\n\n`;
162
+
163
+ // LoginParams interface
164
+ output += `export interface LoginParams {\n`;
165
+ for (const [paramName, paramType] of Object.entries(auth.loginParams)) {
166
+ const tsType = PARAM_TYPE_MAP[paramType] || paramType;
167
+ output += ` ${paramName}: ${tsType};\n`;
168
+ }
169
+ output += `}\n\n`;
170
+
171
+ // LoginResult interface
172
+ output += `export interface LoginResult extends AuthUser {\n`;
173
+ output += ` token: string;\n`;
174
+ output += `}\n\n`;
175
+
176
+ // RegisterParams interface
177
+ output += `export interface RegisterParams {\n`;
178
+ for (const [paramName, paramType] of Object.entries(auth.registerParams)) {
179
+ const tsType = PARAM_TYPE_MAP[paramType] || paramType;
180
+ output += ` ${paramName}: ${tsType};\n`;
181
+ }
182
+ output += `}\n\n`;
183
+
184
+ // RegisterResult interface
185
+ output += `export interface RegisterResult extends AuthUser {\n`;
186
+ output += ` token: string;\n`;
187
+ output += `}\n\n`;
188
+ }
189
+
190
+ // --- Custom Function Types ---
191
+ if (customFunctions) {
192
+ for (const fn of customFunctions) {
193
+ const pascalName = toPascalCase(fn.name);
194
+
195
+ // Generate params interface if params are defined
196
+ if (fn.params && Object.keys(fn.params).length > 0) {
197
+ output += `export interface ${pascalName}Params {\n`;
198
+ for (const [paramName, paramType] of Object.entries(fn.params)) {
199
+ const tsType = PARAM_TYPE_MAP[paramType] || paramType;
200
+ output += ` ${paramName}: ${tsType};\n`;
201
+ }
202
+ output += `}\n\n`;
203
+ }
204
+
205
+ // Generate result interface if returns is an object
206
+ if (fn.returns && typeof fn.returns === 'object') {
207
+ output += `export interface ${pascalName}Result {\n`;
208
+ for (const [fieldName, fieldType] of Object.entries(fn.returns)) {
209
+ const tsType = PARAM_TYPE_MAP[fieldType] || fieldType;
210
+ output += ` ${fieldName}: ${tsType};\n`;
211
+ }
212
+ output += `}\n\n`;
213
+ }
120
214
  }
121
215
  }
122
216
 
@@ -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' }
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' }
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/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
  }