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.
- package/.env.sample +28 -0
- package/compose.yml +28 -0
- package/dist/client/index.ts +1 -0
- package/dist/client/stores/useMyProfileStore.ts +114 -0
- package/dist/client/stores/useOrgDashboardStore.ts +131 -0
- package/dist/client/stores/useVenueDetailStore.ts +117 -0
- package/dist/client/ws.ts +716 -0
- package/dist/db/migrations/000_core.sql +92 -0
- package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
- package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
- package/dist/runtime/manifest.json +1562 -0
- package/docs/README.md +309 -36
- package/docs/feature-requests/applyPatch-bug-report.md +85 -0
- package/docs/feature-requests/connection-ready-profile.md +57 -0
- package/docs/feature-requests/hidden-bug-report.md +111 -0
- package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
- package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
- package/docs/feature-requests/todo.md +146 -0
- package/docs/for_ai.md +653 -0
- package/docs/project-setup.md +456 -0
- package/examples/blog.ts +50 -0
- package/examples/invalid.ts +18 -0
- package/examples/venues.js +485 -0
- package/package.json +23 -60
- package/src/cli/codegen/client.ts +99 -0
- package/src/cli/codegen/manifest.ts +95 -0
- package/src/cli/codegen/pinia.ts +174 -0
- package/src/cli/codegen/realtime.ts +58 -0
- package/src/cli/codegen/sql.ts +698 -0
- package/src/cli/codegen/subscribable_sql.ts +547 -0
- package/src/cli/codegen/subscribable_store.ts +184 -0
- package/src/cli/codegen/types.ts +142 -0
- package/src/cli/compiler/analyzer.ts +52 -0
- package/src/cli/compiler/graph_rules.ts +251 -0
- package/src/cli/compiler/ir.ts +233 -0
- package/src/cli/compiler/loader.ts +132 -0
- package/src/cli/compiler/permissions.ts +227 -0
- package/src/cli/index.ts +166 -0
- package/src/client/index.ts +1 -0
- package/src/client/ws.ts +286 -0
- package/src/runtime/auth.ts +39 -0
- package/src/runtime/db.ts +33 -0
- package/src/runtime/errors.ts +51 -0
- package/src/runtime/index.ts +98 -0
- package/src/runtime/js_functions.ts +63 -0
- package/src/runtime/manifest_loader.ts +29 -0
- package/src/runtime/namespace.ts +483 -0
- package/src/runtime/server.ts +87 -0
- package/src/runtime/ws.ts +197 -0
- package/src/shared/ir.ts +197 -0
- package/tests/client.test.ts +38 -0
- package/tests/codegen.test.ts +71 -0
- package/tests/compiler.test.ts +45 -0
- package/tests/graph_rules.test.ts +173 -0
- package/tests/integration/db.test.ts +174 -0
- package/tests/integration/e2e.test.ts +65 -0
- package/tests/integration/features.test.ts +922 -0
- package/tests/integration/full_stack.test.ts +262 -0
- package/tests/integration/setup.ts +45 -0
- package/tests/ir.test.ts +32 -0
- package/tests/namespace.test.ts +395 -0
- package/tests/permissions.test.ts +55 -0
- package/tests/pinia.test.ts +48 -0
- package/tests/realtime.test.ts +22 -0
- package/tests/runtime.test.ts +80 -0
- package/tests/subscribable_gen.test.ts +72 -0
- package/tests/subscribable_reactivity.test.ts +258 -0
- package/tests/venues_gen.test.ts +25 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/README.md +0 -90
- package/bin/cli.js +0 -727
- package/docs/compiler/ADVANCED_FILTERS.md +0 -183
- package/docs/compiler/CODING_STANDARDS.md +0 -415
- package/docs/compiler/COMPARISON.md +0 -673
- package/docs/compiler/QUICKSTART.md +0 -326
- package/docs/compiler/README.md +0 -134
- package/docs/examples/README.md +0 -38
- package/docs/examples/blog.sql +0 -160
- package/docs/examples/venue-detail-simple.sql +0 -8
- package/docs/examples/venue-detail-subscribable.sql +0 -45
- package/docs/for-ai/claude-guide.md +0 -1210
- package/docs/getting-started/quickstart.md +0 -125
- package/docs/getting-started/subscriptions-quick-start.md +0 -203
- package/docs/getting-started/tutorial.md +0 -1104
- package/docs/guides/atomic-updates.md +0 -299
- package/docs/guides/client-stores.md +0 -730
- package/docs/guides/composite-primary-keys.md +0 -158
- package/docs/guides/custom-functions.md +0 -362
- package/docs/guides/drop-semantics.md +0 -554
- package/docs/guides/field-defaults.md +0 -240
- package/docs/guides/interpreter-vs-compiler.md +0 -237
- package/docs/guides/many-to-many.md +0 -929
- package/docs/guides/subscriptions.md +0 -537
- package/docs/reference/api.md +0 -1373
- package/docs/reference/client.md +0 -224
- package/src/client/stores/index.js +0 -8
- package/src/client/stores/useAppStore.js +0 -285
- package/src/client/stores/useWsStore.js +0 -289
- package/src/client/ws.js +0 -762
- package/src/compiler/cli/compile-example.js +0 -33
- package/src/compiler/cli/compile-subscribable.js +0 -43
- package/src/compiler/cli/debug-compile.js +0 -44
- package/src/compiler/cli/debug-parse.js +0 -26
- package/src/compiler/cli/debug-path-parser.js +0 -18
- package/src/compiler/cli/debug-subscribable-parser.js +0 -21
- package/src/compiler/cli/index.js +0 -174
- package/src/compiler/codegen/auth-codegen.js +0 -153
- package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
- package/src/compiler/codegen/graph-rules-codegen.js +0 -450
- package/src/compiler/codegen/notification-codegen.js +0 -232
- package/src/compiler/codegen/operation-codegen.js +0 -1382
- package/src/compiler/codegen/permission-codegen.js +0 -318
- package/src/compiler/codegen/subscribable-codegen.js +0 -827
- package/src/compiler/compiler.js +0 -371
- package/src/compiler/index.js +0 -11
- package/src/compiler/parser/entity-parser.js +0 -440
- package/src/compiler/parser/path-parser.js +0 -290
- package/src/compiler/parser/subscribable-parser.js +0 -244
- package/src/database/dzql-core.sql +0 -161
- package/src/database/migrations/001_schema.sql +0 -60
- package/src/database/migrations/002_functions.sql +0 -890
- package/src/database/migrations/003_operations.sql +0 -1135
- package/src/database/migrations/004_search.sql +0 -581
- package/src/database/migrations/005_entities.sql +0 -730
- package/src/database/migrations/006_auth.sql +0 -94
- package/src/database/migrations/007_events.sql +0 -133
- package/src/database/migrations/008_hello.sql +0 -18
- package/src/database/migrations/008a_meta.sql +0 -172
- package/src/database/migrations/009_subscriptions.sql +0 -240
- package/src/database/migrations/010_atomic_updates.sql +0 -157
- package/src/database/migrations/010_fix_m2m_events.sql +0 -94
- package/src/index.js +0 -40
- package/src/server/api.js +0 -9
- package/src/server/db.js +0 -442
- package/src/server/index.js +0 -317
- package/src/server/logger.js +0 -259
- package/src/server/mcp.js +0 -594
- package/src/server/meta-route.js +0 -251
- package/src/server/namespace.js +0 -292
- package/src/server/subscriptions.js +0 -351
- 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
|
+
}
|