dzql 0.5.33 → 0.6.1

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.
Files changed (142) hide show
  1. package/.env.sample +28 -0
  2. package/compose.yml +28 -0
  3. package/dist/client/index.ts +1 -0
  4. package/dist/client/stores/useMyProfileStore.ts +114 -0
  5. package/dist/client/stores/useOrgDashboardStore.ts +131 -0
  6. package/dist/client/stores/useVenueDetailStore.ts +117 -0
  7. package/dist/client/ws.ts +716 -0
  8. package/dist/db/migrations/000_core.sql +92 -0
  9. package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
  10. package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
  11. package/dist/runtime/manifest.json +1562 -0
  12. package/docs/README.md +309 -36
  13. package/docs/feature-requests/applyPatch-bug-report.md +85 -0
  14. package/docs/feature-requests/connection-ready-profile.md +57 -0
  15. package/docs/feature-requests/hidden-bug-report.md +111 -0
  16. package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
  17. package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
  18. package/docs/feature-requests/todo.md +146 -0
  19. package/docs/for_ai.md +653 -0
  20. package/docs/project-setup.md +456 -0
  21. package/examples/blog.ts +50 -0
  22. package/examples/invalid.ts +18 -0
  23. package/examples/venues.js +485 -0
  24. package/package.json +23 -60
  25. package/src/cli/codegen/client.ts +99 -0
  26. package/src/cli/codegen/manifest.ts +95 -0
  27. package/src/cli/codegen/pinia.ts +174 -0
  28. package/src/cli/codegen/realtime.ts +58 -0
  29. package/src/cli/codegen/sql.ts +698 -0
  30. package/src/cli/codegen/subscribable_sql.ts +547 -0
  31. package/src/cli/codegen/subscribable_store.ts +184 -0
  32. package/src/cli/codegen/types.ts +142 -0
  33. package/src/cli/compiler/analyzer.ts +52 -0
  34. package/src/cli/compiler/graph_rules.ts +251 -0
  35. package/src/cli/compiler/ir.ts +233 -0
  36. package/src/cli/compiler/loader.ts +132 -0
  37. package/src/cli/compiler/permissions.ts +227 -0
  38. package/src/cli/index.ts +166 -0
  39. package/src/client/index.ts +1 -0
  40. package/src/client/ws.ts +286 -0
  41. package/src/runtime/auth.ts +39 -0
  42. package/src/runtime/db.ts +33 -0
  43. package/src/runtime/errors.ts +51 -0
  44. package/src/runtime/index.ts +98 -0
  45. package/src/runtime/js_functions.ts +63 -0
  46. package/src/runtime/manifest_loader.ts +29 -0
  47. package/src/runtime/namespace.ts +483 -0
  48. package/src/runtime/server.ts +87 -0
  49. package/src/runtime/ws.ts +197 -0
  50. package/src/shared/ir.ts +197 -0
  51. package/tests/client.test.ts +38 -0
  52. package/tests/codegen.test.ts +71 -0
  53. package/tests/compiler.test.ts +45 -0
  54. package/tests/graph_rules.test.ts +173 -0
  55. package/tests/integration/db.test.ts +174 -0
  56. package/tests/integration/e2e.test.ts +65 -0
  57. package/tests/integration/features.test.ts +922 -0
  58. package/tests/integration/full_stack.test.ts +262 -0
  59. package/tests/integration/setup.ts +45 -0
  60. package/tests/ir.test.ts +32 -0
  61. package/tests/namespace.test.ts +395 -0
  62. package/tests/permissions.test.ts +55 -0
  63. package/tests/pinia.test.ts +48 -0
  64. package/tests/realtime.test.ts +22 -0
  65. package/tests/runtime.test.ts +80 -0
  66. package/tests/subscribable_gen.test.ts +72 -0
  67. package/tests/subscribable_reactivity.test.ts +258 -0
  68. package/tests/venues_gen.test.ts +25 -0
  69. package/tsconfig.json +20 -0
  70. package/tsconfig.tsbuildinfo +1 -0
  71. package/README.md +0 -90
  72. package/bin/cli.js +0 -727
  73. package/docs/compiler/ADVANCED_FILTERS.md +0 -183
  74. package/docs/compiler/CODING_STANDARDS.md +0 -415
  75. package/docs/compiler/COMPARISON.md +0 -673
  76. package/docs/compiler/QUICKSTART.md +0 -326
  77. package/docs/compiler/README.md +0 -134
  78. package/docs/examples/README.md +0 -38
  79. package/docs/examples/blog.sql +0 -160
  80. package/docs/examples/venue-detail-simple.sql +0 -8
  81. package/docs/examples/venue-detail-subscribable.sql +0 -45
  82. package/docs/for-ai/claude-guide.md +0 -1210
  83. package/docs/getting-started/quickstart.md +0 -125
  84. package/docs/getting-started/subscriptions-quick-start.md +0 -203
  85. package/docs/getting-started/tutorial.md +0 -1104
  86. package/docs/guides/atomic-updates.md +0 -299
  87. package/docs/guides/client-stores.md +0 -730
  88. package/docs/guides/composite-primary-keys.md +0 -158
  89. package/docs/guides/custom-functions.md +0 -362
  90. package/docs/guides/drop-semantics.md +0 -554
  91. package/docs/guides/field-defaults.md +0 -240
  92. package/docs/guides/interpreter-vs-compiler.md +0 -237
  93. package/docs/guides/many-to-many.md +0 -929
  94. package/docs/guides/subscriptions.md +0 -537
  95. package/docs/reference/api.md +0 -1373
  96. package/docs/reference/client.md +0 -224
  97. package/src/client/stores/index.js +0 -8
  98. package/src/client/stores/useAppStore.js +0 -285
  99. package/src/client/stores/useWsStore.js +0 -289
  100. package/src/client/ws.js +0 -762
  101. package/src/compiler/cli/compile-example.js +0 -33
  102. package/src/compiler/cli/compile-subscribable.js +0 -43
  103. package/src/compiler/cli/debug-compile.js +0 -44
  104. package/src/compiler/cli/debug-parse.js +0 -26
  105. package/src/compiler/cli/debug-path-parser.js +0 -18
  106. package/src/compiler/cli/debug-subscribable-parser.js +0 -21
  107. package/src/compiler/cli/index.js +0 -174
  108. package/src/compiler/codegen/auth-codegen.js +0 -153
  109. package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
  110. package/src/compiler/codegen/graph-rules-codegen.js +0 -450
  111. package/src/compiler/codegen/notification-codegen.js +0 -232
  112. package/src/compiler/codegen/operation-codegen.js +0 -1382
  113. package/src/compiler/codegen/permission-codegen.js +0 -318
  114. package/src/compiler/codegen/subscribable-codegen.js +0 -827
  115. package/src/compiler/compiler.js +0 -371
  116. package/src/compiler/index.js +0 -11
  117. package/src/compiler/parser/entity-parser.js +0 -440
  118. package/src/compiler/parser/path-parser.js +0 -290
  119. package/src/compiler/parser/subscribable-parser.js +0 -244
  120. package/src/database/dzql-core.sql +0 -161
  121. package/src/database/migrations/001_schema.sql +0 -60
  122. package/src/database/migrations/002_functions.sql +0 -890
  123. package/src/database/migrations/003_operations.sql +0 -1135
  124. package/src/database/migrations/004_search.sql +0 -581
  125. package/src/database/migrations/005_entities.sql +0 -730
  126. package/src/database/migrations/006_auth.sql +0 -94
  127. package/src/database/migrations/007_events.sql +0 -133
  128. package/src/database/migrations/008_hello.sql +0 -18
  129. package/src/database/migrations/008a_meta.sql +0 -172
  130. package/src/database/migrations/009_subscriptions.sql +0 -240
  131. package/src/database/migrations/010_atomic_updates.sql +0 -157
  132. package/src/database/migrations/010_fix_m2m_events.sql +0 -94
  133. package/src/index.js +0 -40
  134. package/src/server/api.js +0 -9
  135. package/src/server/db.js +0 -442
  136. package/src/server/index.js +0 -317
  137. package/src/server/logger.js +0 -259
  138. package/src/server/mcp.js +0 -594
  139. package/src/server/meta-route.js +0 -251
  140. package/src/server/namespace.js +0 -292
  141. package/src/server/subscriptions.js +0 -351
  142. package/src/server/ws.js +0 -573
@@ -0,0 +1,63 @@
1
+ // JavaScript Custom Function Registry
2
+ // Allows registering JS/Bun functions that can be called via RPC
3
+
4
+ export interface JsFunctionContext {
5
+ userId: number;
6
+ params: any;
7
+ db: {
8
+ query(sql: string, params?: any[]): Promise<any[]>;
9
+ };
10
+ }
11
+
12
+ export type JsFunctionHandler = (ctx: JsFunctionContext) => Promise<any>;
13
+
14
+ // Registry of JS function handlers
15
+ const jsHandlers: Map<string, JsFunctionHandler> = new Map();
16
+
17
+ /**
18
+ * Register a JavaScript function that can be called via RPC.
19
+ * JS functions take precedence over SQL functions with the same name.
20
+ *
21
+ * @param name - The function name (used in RPC calls)
22
+ * @param handler - Async function that receives context and returns result
23
+ *
24
+ * @example
25
+ * registerJsFunction('calculate_stats', async (ctx) => {
26
+ * const { userId, params, db } = ctx;
27
+ * const rows = await db.query('SELECT COUNT(*) as cnt FROM venues WHERE org_id = $1', [params.org_id]);
28
+ * return { venue_count: rows[0].cnt };
29
+ * });
30
+ */
31
+ export function registerJsFunction(name: string, handler: JsFunctionHandler): void {
32
+ jsHandlers.set(name, handler);
33
+ console.log(`[Runtime] Registered JS function: ${name}`);
34
+ }
35
+
36
+ /**
37
+ * Get a registered JS function handler by name.
38
+ * @returns The handler function or undefined if not registered
39
+ */
40
+ export function getJsFunction(name: string): JsFunctionHandler | undefined {
41
+ return jsHandlers.get(name);
42
+ }
43
+
44
+ /**
45
+ * Check if a JS function is registered.
46
+ */
47
+ export function hasJsFunction(name: string): boolean {
48
+ return jsHandlers.has(name);
49
+ }
50
+
51
+ /**
52
+ * Clear all registered JS functions (useful for testing).
53
+ */
54
+ export function clearJsFunctions(): void {
55
+ jsHandlers.clear();
56
+ }
57
+
58
+ /**
59
+ * Get all registered JS function names.
60
+ */
61
+ export function getJsFunctionNames(): string[] {
62
+ return Array.from(jsHandlers.keys());
63
+ }
@@ -0,0 +1,29 @@
1
+ import { Manifest } from "../cli/codegen/manifest.js";
2
+
3
+ // Global cache for the loaded manifest
4
+ let activeManifest: Manifest | null = null;
5
+
6
+ export function loadManifest(manifest: Manifest) {
7
+ console.log(`[Runtime] Loading manifest v${manifest.version}`);
8
+ activeManifest = manifest;
9
+ }
10
+
11
+ export function getManifest(): Manifest {
12
+ if (!activeManifest) {
13
+ throw new Error("[Runtime] Manifest not loaded.");
14
+ }
15
+ return activeManifest;
16
+ }
17
+
18
+ export function resolveFunction(name: string) {
19
+ const manifest = getManifest();
20
+ const fn = manifest.functions[name];
21
+
22
+ if (!fn) {
23
+ return null;
24
+ }
25
+
26
+ // In a real DB-connected runtime, we would resolve OID here.
27
+ // For now, we return the schema-qualified name.
28
+ return `${fn.schema}.${fn.name}`;
29
+ }
@@ -0,0 +1,483 @@
1
+ /**
2
+ * TZQL Namespace for invokej integration
3
+ *
4
+ * Provides CLI-style access to TZQL operations via the compiled manifest.
5
+ * Each method outputs JSON to console and closes the connection before returning.
6
+ *
7
+ * Setup - add to your tasks.js:
8
+ * ```js
9
+ * import { TzqlNamespace } from 'tzql/namespace';
10
+ *
11
+ * export class Tasks {
12
+ * constructor() {
13
+ * this.tzql = new TzqlNamespace();
14
+ * }
15
+ * }
16
+ * ```
17
+ *
18
+ * Available Commands:
19
+ *
20
+ * Discovery:
21
+ * invj tzql:entities # List all entities
22
+ * invj tzql:subscribables # List all subscribables
23
+ * invj tzql:functions # List all manifest functions
24
+ *
25
+ * Entity CRUD:
26
+ * invj tzql:search venues '{"query": "test"}' # Search with filters
27
+ * invj tzql:get venues '{"id": 1}' # Get by primary key
28
+ * invj tzql:save venues '{"name": "New", "org_id": 1}' # Create (no id)
29
+ * invj tzql:save venues '{"id": 1, "name": "Updated"}' # Update (with id)
30
+ * invj tzql:delete venues '{"id": 1}' # Delete by primary key
31
+ * invj tzql:lookup venues '{"query": "test"}' # Lookup for dropdowns
32
+ *
33
+ * Subscribables:
34
+ * invj tzql:subscribe venue_detail '{"venue_id": 1}' # Get snapshot
35
+ *
36
+ * Ad-hoc Function Calls:
37
+ * invj tzql:call login_user '{"email": "x", "password": "y"}'
38
+ * invj tzql:call register_user '{"email": "x", "password": "y"}'
39
+ * invj tzql:call get_venue_detail '{"venue_id": 1}'
40
+ * invj tzql:call save_venues '{"name": "Test", "org_id": 1}'
41
+ *
42
+ * Environment:
43
+ * DATABASE_URL - PostgreSQL connection string (default: postgres://localhost:5432/dzql)
44
+ *
45
+ * Requirements:
46
+ * - Run 'tzql compile' first to generate dist/runtime/manifest.json
47
+ */
48
+
49
+ import postgres from "postgres";
50
+ import { readFileSync, existsSync } from "fs";
51
+ import { join, resolve } from "path";
52
+ import type { Manifest, FunctionDef } from "../cli/codegen/manifest.js";
53
+
54
+ // Default user for CLI operations
55
+ const DEFAULT_USER_ID = 1;
56
+
57
+ /**
58
+ * Load manifest from MANIFEST_PATH env var or default locations
59
+ */
60
+ function loadManifestFromDisk(): Manifest {
61
+ // First check MANIFEST_PATH env var (like the runtime does)
62
+ const envPath = process.env.MANIFEST_PATH;
63
+ if (envPath) {
64
+ const resolvedPath = resolve(process.cwd(), envPath);
65
+ if (existsSync(resolvedPath)) {
66
+ const content = readFileSync(resolvedPath, "utf-8");
67
+ return JSON.parse(content);
68
+ }
69
+ throw new Error(
70
+ `Manifest not found at MANIFEST_PATH: ${resolvedPath}`
71
+ );
72
+ }
73
+
74
+ // Fall back to default paths
75
+ const paths = [
76
+ join(process.cwd(), "dist/runtime/manifest.json"),
77
+ join(process.cwd(), "packages/tzql/dist/runtime/manifest.json"),
78
+ ];
79
+
80
+ for (const path of paths) {
81
+ if (existsSync(path)) {
82
+ const content = readFileSync(path, "utf-8");
83
+ return JSON.parse(content);
84
+ }
85
+ }
86
+
87
+ throw new Error(
88
+ "Manifest not found. Set MANIFEST_PATH env var or run 'tzql compile' to generate dist/runtime/manifest.json"
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Discover available entities from the manifest
94
+ */
95
+ function discoverEntities(manifest: Manifest): Record<string, { label: string; description: string }> {
96
+ const entities: Record<string, { label: string; description: string }> = {};
97
+
98
+ for (const [name, entity] of Object.entries(manifest.entities || {})) {
99
+ entities[name] = {
100
+ label: (entity as any).labelField || "id",
101
+ description: `Entity: ${name} (compiled mode)`,
102
+ };
103
+ }
104
+
105
+ return entities;
106
+ }
107
+
108
+ /**
109
+ * Discover available subscribables from the manifest
110
+ */
111
+ function discoverSubscribables(manifest: Manifest): Record<string, { params: Record<string, string>; description: string }> {
112
+ const subscribables: Record<string, { params: Record<string, string>; description: string }> = {};
113
+
114
+ for (const [name, sub] of Object.entries(manifest.subscribables || {})) {
115
+ subscribables[name] = {
116
+ params: (sub as any).params || {},
117
+ description: `Subscribable: ${name}`,
118
+ };
119
+ }
120
+
121
+ return subscribables;
122
+ }
123
+
124
+ /**
125
+ * TZQL operations namespace for invokej
126
+ */
127
+ export class TzqlNamespace {
128
+ private userId: number;
129
+ private sql: postgres.Sql | null = null;
130
+ private manifest: Manifest | null = null;
131
+
132
+ constructor(userId: number = DEFAULT_USER_ID) {
133
+ this.userId = userId;
134
+ }
135
+
136
+ private async init(): Promise<{ sql: postgres.Sql; manifest: Manifest }> {
137
+ if (!this.sql) {
138
+ const connectionString = process.env.DATABASE_URL || "postgres://localhost:5432/dzql";
139
+ this.sql = postgres(connectionString, {
140
+ max: 1,
141
+ idle_timeout: 5,
142
+ onnotice: () => {},
143
+ });
144
+ }
145
+ if (!this.manifest) {
146
+ this.manifest = loadManifestFromDisk();
147
+ }
148
+ return { sql: this.sql, manifest: this.manifest };
149
+ }
150
+
151
+ private async cleanup(): Promise<void> {
152
+ if (this.sql) {
153
+ await this.sql.end();
154
+ this.sql = null;
155
+ }
156
+ }
157
+
158
+ private async executeFunction(
159
+ fnName: string,
160
+ params: any
161
+ ): Promise<any> {
162
+ const { sql, manifest } = await this.init();
163
+
164
+ const fnDef = manifest.functions[fnName];
165
+ if (!fnDef) {
166
+ throw new Error(`Function '${fnName}' not found in manifest`);
167
+ }
168
+
169
+ const qualifiedName = `${fnDef.schema}.${fnDef.name}`;
170
+
171
+ // Build SQL params based on function signature
172
+ const dbParams: any[] = [];
173
+ const sqlArgs: string[] = [];
174
+
175
+ for (const arg of fnDef.args) {
176
+ if (arg === "p_user_id") {
177
+ dbParams.push(this.userId);
178
+ sqlArgs.push(`$${dbParams.length}`);
179
+ } else if (["p_data", "p_pk", "p_query", "p_params"].includes(arg)) {
180
+ // Pass the object directly - postgres.js will handle JSON serialization
181
+ dbParams.push(params);
182
+ sqlArgs.push(`$${dbParams.length}::jsonb`);
183
+ } else {
184
+ dbParams.push(null);
185
+ sqlArgs.push(`$${dbParams.length}`);
186
+ }
187
+ }
188
+
189
+ const query = `SELECT ${qualifiedName}(${sqlArgs.join(", ")}) as result`;
190
+ const rows = await sql.unsafe(query, dbParams);
191
+ return rows[0]?.result;
192
+ }
193
+
194
+ /**
195
+ * List all available entities
196
+ */
197
+ async entities(_context?: any): Promise<void> {
198
+ try {
199
+ const { manifest } = await this.init();
200
+ const entities = discoverEntities(manifest);
201
+ console.log(JSON.stringify({ success: true, entities }, null, 2));
202
+ await this.cleanup();
203
+ } catch (error: any) {
204
+ console.error(JSON.stringify({ success: false, error: error.message }, null, 2));
205
+ await this.cleanup();
206
+ process.exit(1);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * List all available subscribables
212
+ */
213
+ async subscribables(_context?: any): Promise<void> {
214
+ try {
215
+ const { manifest } = await this.init();
216
+ const subscribables = discoverSubscribables(manifest);
217
+ console.log(JSON.stringify({ success: true, subscribables }, null, 2));
218
+ await this.cleanup();
219
+ } catch (error: any) {
220
+ console.error(JSON.stringify({ success: false, error: error.message }, null, 2));
221
+ await this.cleanup();
222
+ process.exit(1);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Search an entity
228
+ * @example invj tzql:search venues '{"query": "test"}'
229
+ */
230
+ async search(_context: any, entity?: string, argsJson: string = "{}"): Promise<void> {
231
+ if (!entity) {
232
+ console.error("Error: entity name required");
233
+ console.error("Usage: invj tzql:search <entity> '<json_args>'");
234
+ console.error('Example: invj tzql:search venues \'{"query": "test"}\'');
235
+ await this.cleanup();
236
+ process.exit(1);
237
+ }
238
+
239
+ let args: any;
240
+ try {
241
+ args = JSON.parse(argsJson);
242
+ } catch {
243
+ console.error("Error: arguments must be valid JSON");
244
+ await this.cleanup();
245
+ process.exit(1);
246
+ }
247
+
248
+ try {
249
+ const result = await this.executeFunction(`search_${entity}`, args);
250
+ console.log(JSON.stringify({ success: true, result }, null, 2));
251
+ await this.cleanup();
252
+ } catch (error: any) {
253
+ console.error(JSON.stringify({ success: false, error: error.message }, null, 2));
254
+ await this.cleanup();
255
+ process.exit(1);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Get entity by ID
261
+ * @example invj tzql:get venues '{"id": 1}'
262
+ */
263
+ async get(_context: any, entity?: string, argsJson: string = "{}"): Promise<void> {
264
+ if (!entity) {
265
+ console.error("Error: entity name required");
266
+ console.error("Usage: invj tzql:get <entity> '<json_args>'");
267
+ console.error('Example: invj tzql:get venues \'{"id": 1}\'');
268
+ await this.cleanup();
269
+ process.exit(1);
270
+ }
271
+
272
+ let args: any;
273
+ try {
274
+ args = JSON.parse(argsJson);
275
+ } catch {
276
+ console.error("Error: arguments must be valid JSON");
277
+ await this.cleanup();
278
+ process.exit(1);
279
+ }
280
+
281
+ try {
282
+ const result = await this.executeFunction(`get_${entity}`, args);
283
+ console.log(JSON.stringify({ success: true, result }, null, 2));
284
+ await this.cleanup();
285
+ } catch (error: any) {
286
+ console.error(JSON.stringify({ success: false, error: error.message }, null, 2));
287
+ await this.cleanup();
288
+ process.exit(1);
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Save (create or update) entity
294
+ * @example invj tzql:save venues '{"name": "New Venue", "org_id": 1}'
295
+ */
296
+ async save(_context: any, entity?: string, argsJson: string = "{}"): Promise<void> {
297
+ if (!entity) {
298
+ console.error("Error: entity name required");
299
+ console.error("Usage: invj tzql:save <entity> '<json_args>'");
300
+ console.error('Example: invj tzql:save venues \'{"name": "Test Venue", "org_id": 1}\'');
301
+ await this.cleanup();
302
+ process.exit(1);
303
+ }
304
+
305
+ let args: any;
306
+ try {
307
+ args = JSON.parse(argsJson);
308
+ } catch {
309
+ console.error("Error: arguments must be valid JSON");
310
+ await this.cleanup();
311
+ process.exit(1);
312
+ }
313
+
314
+ try {
315
+ const result = await this.executeFunction(`save_${entity}`, args);
316
+ console.log(JSON.stringify({ success: true, result }, null, 2));
317
+ await this.cleanup();
318
+ } catch (error: any) {
319
+ console.error(JSON.stringify({ success: false, error: error.message }, null, 2));
320
+ await this.cleanup();
321
+ process.exit(1);
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Delete entity by ID
327
+ * @example invj tzql:delete venues '{"id": 1}'
328
+ */
329
+ async delete(_context: any, entity?: string, argsJson: string = "{}"): Promise<void> {
330
+ if (!entity) {
331
+ console.error("Error: entity name required");
332
+ console.error("Usage: invj tzql:delete <entity> '<json_args>'");
333
+ console.error('Example: invj tzql:delete venues \'{"id": 1}\'');
334
+ await this.cleanup();
335
+ process.exit(1);
336
+ }
337
+
338
+ let args: any;
339
+ try {
340
+ args = JSON.parse(argsJson);
341
+ } catch {
342
+ console.error("Error: arguments must be valid JSON");
343
+ await this.cleanup();
344
+ process.exit(1);
345
+ }
346
+
347
+ try {
348
+ const result = await this.executeFunction(`delete_${entity}`, args);
349
+ console.log(JSON.stringify({ success: true, result }, null, 2));
350
+ await this.cleanup();
351
+ } catch (error: any) {
352
+ console.error(JSON.stringify({ success: false, error: error.message }, null, 2));
353
+ await this.cleanup();
354
+ process.exit(1);
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Lookup entity (for dropdowns/autocomplete)
360
+ * @example invj tzql:lookup organisations '{"query": "acme"}'
361
+ */
362
+ async lookup(_context: any, entity?: string, argsJson: string = "{}"): Promise<void> {
363
+ if (!entity) {
364
+ console.error("Error: entity name required");
365
+ console.error("Usage: invj tzql:lookup <entity> '<json_args>'");
366
+ console.error('Example: invj tzql:lookup organisations \'{"query": "acme"}\'');
367
+ await this.cleanup();
368
+ process.exit(1);
369
+ }
370
+
371
+ let args: any;
372
+ try {
373
+ args = JSON.parse(argsJson);
374
+ } catch {
375
+ console.error("Error: arguments must be valid JSON");
376
+ await this.cleanup();
377
+ process.exit(1);
378
+ }
379
+
380
+ try {
381
+ const result = await this.executeFunction(`lookup_${entity}`, args);
382
+ console.log(JSON.stringify({ success: true, result }, null, 2));
383
+ await this.cleanup();
384
+ } catch (error: any) {
385
+ console.error(JSON.stringify({ success: false, error: error.message }, null, 2));
386
+ await this.cleanup();
387
+ process.exit(1);
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Get subscribable snapshot
393
+ * @example invj tzql:subscribe venue_detail '{"venue_id": 1}'
394
+ */
395
+ async subscribe(_context: any, name?: string, argsJson: string = "{}"): Promise<void> {
396
+ if (!name) {
397
+ console.error("Error: subscribable name required");
398
+ console.error("Usage: invj tzql:subscribe <name> '<json_args>'");
399
+ console.error('Example: invj tzql:subscribe venue_detail \'{"venue_id": 1}\'');
400
+ await this.cleanup();
401
+ process.exit(1);
402
+ }
403
+
404
+ let args: any;
405
+ try {
406
+ args = JSON.parse(argsJson);
407
+ } catch {
408
+ console.error("Error: arguments must be valid JSON");
409
+ await this.cleanup();
410
+ process.exit(1);
411
+ }
412
+
413
+ try {
414
+ const result = await this.executeFunction(`get_${name}`, args);
415
+ console.log(JSON.stringify({ success: true, result }, null, 2));
416
+ await this.cleanup();
417
+ } catch (error: any) {
418
+ console.error(JSON.stringify({ success: false, error: error.message }, null, 2));
419
+ await this.cleanup();
420
+ process.exit(1);
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Call any function in the manifest by name
426
+ * @example invj tzql:call login_user '{"email": "test@example.com", "password": "secret"}'
427
+ * @example invj tzql:call get_venue_detail '{"venue_id": 1}'
428
+ */
429
+ async call(_context: any, funcName?: string, argsJson: string = "{}"): Promise<void> {
430
+ if (!funcName) {
431
+ console.error("Error: function name required");
432
+ console.error("Usage: invj tzql:call <function_name> '<json_args>'");
433
+ console.error('Example: invj tzql:call login_user \'{"email": "test@example.com", "password": "secret"}\'');
434
+ console.error('Example: invj tzql:call get_venue_detail \'{"venue_id": 1}\'');
435
+ await this.cleanup();
436
+ process.exit(1);
437
+ }
438
+
439
+ let args: any;
440
+ try {
441
+ args = JSON.parse(argsJson);
442
+ } catch {
443
+ console.error("Error: arguments must be valid JSON");
444
+ await this.cleanup();
445
+ process.exit(1);
446
+ }
447
+
448
+ try {
449
+ const result = await this.executeFunction(funcName, args);
450
+ console.log(JSON.stringify({ success: true, result }, null, 2));
451
+ await this.cleanup();
452
+ } catch (error: any) {
453
+ console.error(JSON.stringify({ success: false, error: error.message }, null, 2));
454
+ await this.cleanup();
455
+ process.exit(1);
456
+ }
457
+ }
458
+
459
+ /**
460
+ * List all available functions in the manifest
461
+ * @example invj tzql:functions
462
+ */
463
+ async functions(_context?: any): Promise<void> {
464
+ try {
465
+ const { manifest } = await this.init();
466
+ const functions: Record<string, { args: string[]; returnType: string }> = {};
467
+
468
+ for (const [name, fn] of Object.entries(manifest.functions)) {
469
+ functions[name] = {
470
+ args: fn.args,
471
+ returnType: fn.returnType,
472
+ };
473
+ }
474
+
475
+ console.log(JSON.stringify({ success: true, functions }, null, 2));
476
+ await this.cleanup();
477
+ } catch (error: any) {
478
+ console.error(JSON.stringify({ success: false, error: error.message }, null, 2));
479
+ await this.cleanup();
480
+ process.exit(1);
481
+ }
482
+ }
483
+ }
@@ -0,0 +1,87 @@
1
+ import { resolveFunction, getManifest } from "./manifest_loader.js";
2
+ import { mapDatabaseError } from "./errors.js";
3
+ import { getJsFunction, hasJsFunction } from "./js_functions.js";
4
+
5
+ // Mock DB client interface
6
+ interface DBClient {
7
+ query(text: string, params: any[]): Promise<any>;
8
+ }
9
+
10
+ export async function handleRequest(
11
+ db: DBClient,
12
+ method: string,
13
+ params: any,
14
+ userId: number
15
+ ) {
16
+ // 1. Check for JS function handler first (takes precedence)
17
+ if (hasJsFunction(method)) {
18
+ const handler = getJsFunction(method)!;
19
+ console.log(`[Runtime] Executing JS function: ${method}`);
20
+
21
+ try {
22
+ const result = await handler({
23
+ userId,
24
+ params,
25
+ db: {
26
+ query: (sql: string, sqlParams?: any[]) => db.query(sql, sqlParams || [])
27
+ }
28
+ });
29
+ return result;
30
+ } catch (err: any) {
31
+ console.error(`[Runtime] JS Error executing ${method}:`, err);
32
+ throw err;
33
+ }
34
+ }
35
+
36
+ // 2. Strict Allowlist Check (O(1) Lookup)
37
+ const manifest = getManifest();
38
+ const fnDef = manifest.functions[method];
39
+
40
+ if (!fnDef) {
41
+ throw new Error(`[Runtime] Method '${method}' not found in manifest.`);
42
+ }
43
+
44
+ const qualifiedName = `${fnDef.schema}.${fnDef.name}`;
45
+
46
+ // 3. Argument Validation (Basic)
47
+ // We assume all functions take (p_user_id, p_data/p_pk) for now
48
+ // In reality, we'd check manifest.functions[method].args
49
+
50
+ // 4. Secure Execution
51
+ console.log(`[Runtime] Executing ${qualifiedName}`);
52
+
53
+ try {
54
+ // Construct params array based on manifest definition
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)
58
+
59
+ const dbParams = [];
60
+ const sqlArgs = [];
61
+
62
+ // We strictly map our known runtime values (userId, params) to the function signature
63
+ // This assumes the function signature follows our conventions
64
+ for (const arg of fnDef.args) {
65
+ if (arg === 'p_user_id') {
66
+ dbParams.push(userId);
67
+ sqlArgs.push(`$${dbParams.length}`);
68
+ } else if (arg === 'p_data' || arg === 'p_pk' || arg === 'p_query' || arg === 'p_params') {
69
+ dbParams.push(params);
70
+ sqlArgs.push(`$${dbParams.length}`);
71
+ } else {
72
+ // Unknown arg? Pass null
73
+ dbParams.push(null);
74
+ sqlArgs.push(`$${dbParams.length}`);
75
+ }
76
+ }
77
+
78
+ const sql = `SELECT ${qualifiedName}(${sqlArgs.join(', ')}) as result`;
79
+ const rows = await db.query(sql, dbParams);
80
+ return rows[0].result;
81
+ } catch (err: any) {
82
+ // 5. Error Sanitization
83
+ console.error(`[Runtime] DB Error executing ${method}:`, err);
84
+ const appError = mapDatabaseError(err);
85
+ throw appError;
86
+ }
87
+ }