dzql 0.5.33 → 0.6.0

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 (150) 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 +293 -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 +641 -0
  20. package/docs/project-setup.md +432 -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 +164 -0
  39. package/src/client/index.ts +1 -0
  40. package/src/client/ws.ts +286 -0
  41. package/src/create/.env.example +8 -0
  42. package/src/create/README.md +101 -0
  43. package/src/create/compose.yml +14 -0
  44. package/src/create/domain.ts +153 -0
  45. package/src/create/package.json +24 -0
  46. package/src/create/server.ts +18 -0
  47. package/src/create/setup.sh +11 -0
  48. package/src/create/tsconfig.json +15 -0
  49. package/src/runtime/auth.ts +39 -0
  50. package/src/runtime/db.ts +33 -0
  51. package/src/runtime/errors.ts +51 -0
  52. package/src/runtime/index.ts +98 -0
  53. package/src/runtime/js_functions.ts +63 -0
  54. package/src/runtime/manifest_loader.ts +29 -0
  55. package/src/runtime/namespace.ts +483 -0
  56. package/src/runtime/server.ts +87 -0
  57. package/src/runtime/ws.ts +197 -0
  58. package/src/shared/ir.ts +197 -0
  59. package/tests/client.test.ts +38 -0
  60. package/tests/codegen.test.ts +71 -0
  61. package/tests/compiler.test.ts +45 -0
  62. package/tests/graph_rules.test.ts +173 -0
  63. package/tests/integration/db.test.ts +174 -0
  64. package/tests/integration/e2e.test.ts +65 -0
  65. package/tests/integration/features.test.ts +922 -0
  66. package/tests/integration/full_stack.test.ts +262 -0
  67. package/tests/integration/setup.ts +45 -0
  68. package/tests/ir.test.ts +32 -0
  69. package/tests/namespace.test.ts +395 -0
  70. package/tests/permissions.test.ts +55 -0
  71. package/tests/pinia.test.ts +48 -0
  72. package/tests/realtime.test.ts +22 -0
  73. package/tests/runtime.test.ts +80 -0
  74. package/tests/subscribable_gen.test.ts +72 -0
  75. package/tests/subscribable_reactivity.test.ts +258 -0
  76. package/tests/venues_gen.test.ts +25 -0
  77. package/tsconfig.json +20 -0
  78. package/tsconfig.tsbuildinfo +1 -0
  79. package/README.md +0 -90
  80. package/bin/cli.js +0 -727
  81. package/docs/compiler/ADVANCED_FILTERS.md +0 -183
  82. package/docs/compiler/CODING_STANDARDS.md +0 -415
  83. package/docs/compiler/COMPARISON.md +0 -673
  84. package/docs/compiler/QUICKSTART.md +0 -326
  85. package/docs/compiler/README.md +0 -134
  86. package/docs/examples/README.md +0 -38
  87. package/docs/examples/blog.sql +0 -160
  88. package/docs/examples/venue-detail-simple.sql +0 -8
  89. package/docs/examples/venue-detail-subscribable.sql +0 -45
  90. package/docs/for-ai/claude-guide.md +0 -1210
  91. package/docs/getting-started/quickstart.md +0 -125
  92. package/docs/getting-started/subscriptions-quick-start.md +0 -203
  93. package/docs/getting-started/tutorial.md +0 -1104
  94. package/docs/guides/atomic-updates.md +0 -299
  95. package/docs/guides/client-stores.md +0 -730
  96. package/docs/guides/composite-primary-keys.md +0 -158
  97. package/docs/guides/custom-functions.md +0 -362
  98. package/docs/guides/drop-semantics.md +0 -554
  99. package/docs/guides/field-defaults.md +0 -240
  100. package/docs/guides/interpreter-vs-compiler.md +0 -237
  101. package/docs/guides/many-to-many.md +0 -929
  102. package/docs/guides/subscriptions.md +0 -537
  103. package/docs/reference/api.md +0 -1373
  104. package/docs/reference/client.md +0 -224
  105. package/src/client/stores/index.js +0 -8
  106. package/src/client/stores/useAppStore.js +0 -285
  107. package/src/client/stores/useWsStore.js +0 -289
  108. package/src/client/ws.js +0 -762
  109. package/src/compiler/cli/compile-example.js +0 -33
  110. package/src/compiler/cli/compile-subscribable.js +0 -43
  111. package/src/compiler/cli/debug-compile.js +0 -44
  112. package/src/compiler/cli/debug-parse.js +0 -26
  113. package/src/compiler/cli/debug-path-parser.js +0 -18
  114. package/src/compiler/cli/debug-subscribable-parser.js +0 -21
  115. package/src/compiler/cli/index.js +0 -174
  116. package/src/compiler/codegen/auth-codegen.js +0 -153
  117. package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
  118. package/src/compiler/codegen/graph-rules-codegen.js +0 -450
  119. package/src/compiler/codegen/notification-codegen.js +0 -232
  120. package/src/compiler/codegen/operation-codegen.js +0 -1382
  121. package/src/compiler/codegen/permission-codegen.js +0 -318
  122. package/src/compiler/codegen/subscribable-codegen.js +0 -827
  123. package/src/compiler/compiler.js +0 -371
  124. package/src/compiler/index.js +0 -11
  125. package/src/compiler/parser/entity-parser.js +0 -440
  126. package/src/compiler/parser/path-parser.js +0 -290
  127. package/src/compiler/parser/subscribable-parser.js +0 -244
  128. package/src/database/dzql-core.sql +0 -161
  129. package/src/database/migrations/001_schema.sql +0 -60
  130. package/src/database/migrations/002_functions.sql +0 -890
  131. package/src/database/migrations/003_operations.sql +0 -1135
  132. package/src/database/migrations/004_search.sql +0 -581
  133. package/src/database/migrations/005_entities.sql +0 -730
  134. package/src/database/migrations/006_auth.sql +0 -94
  135. package/src/database/migrations/007_events.sql +0 -133
  136. package/src/database/migrations/008_hello.sql +0 -18
  137. package/src/database/migrations/008a_meta.sql +0 -172
  138. package/src/database/migrations/009_subscriptions.sql +0 -240
  139. package/src/database/migrations/010_atomic_updates.sql +0 -157
  140. package/src/database/migrations/010_fix_m2m_events.sql +0 -94
  141. package/src/index.js +0 -40
  142. package/src/server/api.js +0 -9
  143. package/src/server/db.js +0 -442
  144. package/src/server/index.js +0 -317
  145. package/src/server/logger.js +0 -259
  146. package/src/server/mcp.js +0 -594
  147. package/src/server/meta-route.js +0 -251
  148. package/src/server/namespace.js +0 -292
  149. package/src/server/subscriptions.js +0 -351
  150. package/src/server/ws.js +0 -573
@@ -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
+ }