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 +1 -1
- package/src/cli/codegen/client.ts +60 -8
- package/src/cli/codegen/manifest.ts +9 -5
- package/src/cli/codegen/sql.ts +74 -33
- package/src/cli/codegen/types.ts +101 -4
- package/src/cli/compiler/ir.ts +58 -3
- package/src/cli/index.ts +9 -5
- package/src/runtime/server.ts +10 -2
- 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 {
|
|
@@ -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: ['
|
|
23
|
-
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' }
|
|
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
|
}
|
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
|
@@ -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
|
|
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', 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
|
-
|
|
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);
|
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
|
}
|