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 +1 -1
- package/src/cli/codegen/client.ts +60 -8
- package/src/cli/codegen/manifest.ts +6 -2
- package/src/cli/codegen/types.ts +97 -3
- package/src/cli/compiler/ir.ts +58 -3
- package/src/shared/ir.ts +30 -0
package/package.json
CHANGED
|
@@ -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
|
-
//
|
|
19
|
-
const
|
|
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
|
-
|
|
39
|
-
|
|
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 (
|
|
76
|
+
if (subscribableExists) {
|
|
42
77
|
paramType = `${pascalEntity}Params`;
|
|
43
|
-
returnType =
|
|
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
|
-
|
|
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
|
}
|
package/src/cli/codegen/types.ts
CHANGED
|
@@ -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
|
|
package/src/cli/compiler/ir.ts
CHANGED
|
@@ -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
|
}
|