@gonvex/cli 0.4.0 → 0.5.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.
@@ -122,6 +122,56 @@ const validateReplicaCollection = (value, path) => {
122
122
  if (value.mode !== void 0 && value.mode !== "eager" && value.mode !== "progressive") throw new Error(`replica collection ${path} has an invalid completeness mode`);
123
123
  if (value.orderDirection !== void 0 && value.orderDirection !== "asc" && value.orderDirection !== "desc") throw new Error(`replica collection ${path} has an invalid order direction`);
124
124
  };
125
+ const validateActionCapabilities = (profile, value, path) => {
126
+ if (value === void 0) return;
127
+ if (!isRecord(value)) throw new Error(`action ${path} capabilities must be an object`);
128
+ const allowed = /* @__PURE__ */ new Set([
129
+ "networkOrigins",
130
+ "secrets",
131
+ "tools",
132
+ "scheduler",
133
+ "storage",
134
+ "sandbox",
135
+ "functions"
136
+ ]);
137
+ for (const field of Object.keys(value)) if (!allowed.has(field)) throw new Error(`action ${path} capabilities has unsupported field ${field}`);
138
+ if (value.networkOrigins !== void 0) {
139
+ if (!Array.isArray(value.networkOrigins) || value.networkOrigins.length === 0) throw new Error(`action ${path} networkOrigins must be a non-empty array`);
140
+ const seen = /* @__PURE__ */ new Set();
141
+ for (const origin of value.networkOrigins) {
142
+ if (typeof origin !== "string") throw new Error(`action ${path} networkOrigins must contain strings`);
143
+ let parsed;
144
+ try {
145
+ parsed = new URL(origin);
146
+ } catch {
147
+ throw new Error(`action ${path} network origin ${JSON.stringify(origin)} is invalid`);
148
+ }
149
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:" || parsed.origin !== origin || parsed.username || parsed.password) throw new Error(`action ${path} network origin ${JSON.stringify(origin)} must be an exact HTTP(S) origin`);
150
+ if (seen.has(origin)) throw new Error(`action ${path} declares duplicate network origin ${origin}`);
151
+ seen.add(origin);
152
+ }
153
+ }
154
+ if (value.secrets !== void 0) {
155
+ if (!Array.isArray(value.secrets) || value.secrets.some((name) => typeof name !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(name))) throw new Error(`action ${path} secrets must be uppercase environment names`);
156
+ }
157
+ if (value.tools !== void 0) {
158
+ if (profile !== "agent") throw new Error(`action ${path} tools require profile "agent"`);
159
+ if (!isRecord(value.tools) || Object.keys(value.tools).length === 0) throw new Error(`agent action ${path} tools must be a non-empty object`);
160
+ for (const [name, binding] of Object.entries(value.tools)) if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) || !isRecord(binding) || binding.kind !== "query" && binding.kind !== "reducer" || typeof binding.function !== "string" || !binding.function.trim()) throw new Error(`agent action ${path} has an invalid tool binding ${JSON.stringify(name)}`);
161
+ }
162
+ if (value.scheduler !== void 0 && value.scheduler !== true) throw new Error(`action ${path} scheduler must be true when declared`);
163
+ if (value.storage !== void 0 && value.storage !== true) throw new Error(`action ${path} storage must be true when declared`);
164
+ if (value.functions !== void 0) {
165
+ if (profile !== "agent") throw new Error(`action ${path} functions require profile "agent"`);
166
+ if (value.functions !== true) throw new Error(`action ${path} functions must be true when declared`);
167
+ }
168
+ if (value.sandbox !== void 0) {
169
+ if (profile !== "agent") throw new Error(`action ${path} sandbox requires profile "agent"`);
170
+ if (!isRecord(value.sandbox)) throw new Error(`action ${path} sandbox must be an object`);
171
+ for (const field of Object.keys(value.sandbox)) if (field !== "duckdb") throw new Error(`action ${path} sandbox has unsupported field ${field}`);
172
+ if (value.sandbox.duckdb !== void 0 && value.sandbox.duckdb !== true) throw new Error(`action ${path} sandbox.duckdb must be true when declared`);
173
+ }
174
+ };
125
175
  const validateStructuredQueryPlan = (value, path) => {
126
176
  if (!isRecord(value) || typeof value.table !== "string" || !value.table.trim() || typeof value.key !== "string" || !value.key.trim() || !Array.isArray(value.columns) || value.columns.length === 0 || value.columns.some((column) => typeof column !== "string" || !column.trim())) throw new Error(`one-shot query ${path} requires a structured live query plan with a table, key, and columns`);
127
177
  if (!value.columns.includes(value.key)) throw new Error(`one-shot query ${path} live query plan columns must include its key`);
@@ -246,24 +296,56 @@ const validateVisibilityPlan = (value, path) => {
246
296
  const setPath = `${path}.sets.${name}`;
247
297
  validateExactObject(candidate, setPath, [
248
298
  "table",
299
+ "alias",
249
300
  "select",
301
+ "selectFrom",
250
302
  "joins",
251
303
  "where"
252
304
  ]);
253
305
  requireVisibilityString(candidate.table, `${setPath}.table`);
306
+ if (candidate.alias !== void 0) requireVisibilityString(candidate.alias, `${setPath}.alias`);
254
307
  requireVisibilityString(candidate.select, `${setPath}.select`);
308
+ if (candidate.selectFrom !== void 0) requireVisibilityString(candidate.selectFrom, `${setPath}.selectFrom`);
255
309
  if (!Array.isArray(candidate.joins)) throw new Error(`${setPath}.joins must be an array`);
256
- candidate.joins.forEach((join, index) => {
310
+ const joins = candidate.joins;
311
+ joins.forEach((join, index) => {
257
312
  const joinPath = `${setPath}.joins[${index}]`;
258
313
  validateExactObject(join, joinPath, [
259
314
  "table",
315
+ "alias",
316
+ "leftAlias",
260
317
  "leftColumn",
261
318
  "rightColumn"
262
319
  ]);
263
320
  requireVisibilityString(join.table, `${joinPath}.table`);
321
+ if (join.alias !== void 0) requireVisibilityString(join.alias, `${joinPath}.alias`);
322
+ if (join.leftAlias !== void 0) requireVisibilityString(join.leftAlias, `${joinPath}.leftAlias`);
264
323
  requireVisibilityString(join.leftColumn, `${joinPath}.leftColumn`);
265
324
  requireVisibilityString(join.rightColumn, `${joinPath}.rightColumn`);
266
325
  });
326
+ const occurrences = [{
327
+ table: candidate.table,
328
+ alias: candidate.alias ?? candidate.table,
329
+ explicit: candidate.alias !== void 0
330
+ }, ...joins.map((join) => ({
331
+ table: join.table,
332
+ alias: join.alias ?? join.table,
333
+ explicit: join.alias !== void 0
334
+ }))];
335
+ const aliases = /* @__PURE__ */ new Set();
336
+ const tableCounts = /* @__PURE__ */ new Map();
337
+ for (const occurrence of occurrences) tableCounts.set(occurrence.table, (tableCounts.get(occurrence.table) ?? 0) + 1);
338
+ occurrences.forEach((occurrence, index) => {
339
+ if ((tableCounts.get(occurrence.table) ?? 0) > 1 && !occurrence.explicit) throw new Error(`${setPath} repeats table ${occurrence.table}; every occurrence requires an explicit alias`);
340
+ if (aliases.has(occurrence.alias)) throw new Error(`${setPath} repeats logical alias ${occurrence.alias}`);
341
+ if (index > 0) {
342
+ const leftAlias = joins[index - 1].leftAlias;
343
+ if (leftAlias !== void 0 && !aliases.has(leftAlias)) throw new Error(`${setPath}.joins[${index - 1}].leftAlias must reference an earlier occurrence`);
344
+ }
345
+ aliases.add(occurrence.alias);
346
+ });
347
+ const selectFrom = candidate.selectFrom ?? occurrences[0].alias;
348
+ if (!aliases.has(selectFrom)) throw new Error(`${setPath}.selectFrom references unknown alias ${selectFrom}`);
267
349
  if (!Array.isArray(candidate.where)) throw new Error(`${setPath}.where must be an array`);
268
350
  candidate.where.forEach((constraint, index) => {
269
351
  const constraintPath = `${setPath}.where[${index}]`;
@@ -273,6 +355,7 @@ const validateVisibilityPlan = (value, path) => {
273
355
  "context"
274
356
  ]);
275
357
  requireVisibilityString(constraint.table, `${constraintPath}.table`);
358
+ if (!aliases.has(constraint.table)) throw new Error(`${constraintPath}.table references unknown alias ${constraint.table}`);
276
359
  requireVisibilityString(constraint.column, `${constraintPath}.column`);
277
360
  validateVisibilityContext(constraint.context, `${constraintPath}.context`);
278
361
  });
@@ -320,6 +403,7 @@ const queryDefinition = (options, deliveryOverride) => {
320
403
  }
321
404
  return freeze({
322
405
  kind: "query",
406
+ internal: options.internal,
323
407
  delivery,
324
408
  liveQueryPlan,
325
409
  replica,
@@ -327,6 +411,10 @@ const queryDefinition = (options, deliveryOverride) => {
327
411
  handler: options.run
328
412
  });
329
413
  };
414
+ /** Declare an executable one-shot, live, or replica query export. */
415
+ function query(options = {}) {
416
+ return queryDefinition(options);
417
+ }
330
418
  /** Declare an executable live query export with a structured live plan. */
331
419
  function liveQuery(options = {}) {
332
420
  return queryDefinition({
@@ -350,6 +438,15 @@ const reducerDefinition = (options, internal = false) => {
350
438
  function reducer(options) {
351
439
  return reducerDefinition(options);
352
440
  }
441
+ /** Declare an executable action export. */
442
+ function action(options = {}) {
443
+ validateActionCapabilities(options.profile ?? "standard", options.capabilities, options.name?.trim() || "<export>");
444
+ return freeze({
445
+ kind: "action",
446
+ options: executableOptions(options),
447
+ handler: options.run
448
+ });
449
+ }
353
450
  //#endregion
354
451
  //#region gonvex/messages.ts
355
452
  const messagesVisibility = visibility({
@@ -383,7 +480,44 @@ const list = liveQuery({
383
480
  },
384
481
  run: async () => []
385
482
  });
483
+ const get = query({
484
+ interactive: true,
485
+ description: "Read one visible message by ID.",
486
+ agent: {
487
+ tags: ["messages"],
488
+ confirmation: "none"
489
+ },
490
+ args: schema.object({ id: schema.id("messages") }),
491
+ result: schema.array(schema.object({
492
+ id: schema.id("messages"),
493
+ body: schema.string(),
494
+ author: schema.string(),
495
+ created_at: schema.datetime()
496
+ })),
497
+ liveQueryPlan: {
498
+ table: "messages",
499
+ key: "id",
500
+ columns: [
501
+ "id",
502
+ "body",
503
+ "author",
504
+ "created_at"
505
+ ],
506
+ where: {
507
+ operator: "eq",
508
+ column: "id",
509
+ value: { argument: "id" }
510
+ }
511
+ },
512
+ run: async () => []
513
+ });
386
514
  const send = reducer({
515
+ interactive: true,
516
+ description: "Send a message as the acting tenant member.",
517
+ agent: {
518
+ tags: ["messages"],
519
+ confirmation: "none"
520
+ },
387
521
  args: schema.object({ body: schema.string() }),
388
522
  result: schema.object({
389
523
  id: schema.id("messages"),
@@ -396,12 +530,43 @@ const send = reducer({
396
530
  reason: "server assigns the message id"
397
531
  },
398
532
  nonOptimisticReason: "server assigns the message id",
399
- run: async ({ now }, args) => ({
400
- id: "pending",
401
- body: args.body,
402
- author: "demo-user",
403
- created_at: new Date(now).toISOString()
533
+ run: async ({ db, member }, args) => {
534
+ if (!member || member.status !== "active") throw new Error("active tenant membership required");
535
+ return db.insert("messages", {
536
+ id: crypto.randomUUID(),
537
+ body: args.body,
538
+ author: member.displayName ?? member.id
539
+ });
540
+ }
541
+ });
542
+ const echo = action({
543
+ interactive: true,
544
+ description: "Return the supplied message without changing durable state.",
545
+ agent: {
546
+ tags: ["messages"],
547
+ confirmation: "none"
548
+ },
549
+ args: schema.object({ message: schema.string() }),
550
+ result: schema.object({ message: schema.string() }),
551
+ run: async (_ctx, args) => args
552
+ });
553
+ /** Minimal reference for testing permission-equivalent delegated invocation. */
554
+ const agentInvoke = action({
555
+ profile: "agent",
556
+ interactive: false,
557
+ description: "Invoke one function from the active interactive catalog.",
558
+ capabilities: { functions: true },
559
+ args: schema.object({
560
+ path: schema.string(),
561
+ args: schema.any(),
562
+ artifactHash: schema.string()
563
+ }),
564
+ result: schema.any(),
565
+ run: (ctx, args) => ctx.functions.invoke({
566
+ path: args.path,
567
+ args: args.args,
568
+ artifactHash: args.artifactHash
404
569
  })
405
570
  });
406
571
  //#endregion
407
- export { list, messagesVisibility, send };
572
+ export { agentInvoke, echo, get, list, messagesVisibility, send };
@@ -1,6 +1,6 @@
1
1
  // Generated by gonvex dev. Do not edit.
2
2
 
3
- import type { LiveQueryPlan } from "@gonvex/client";
3
+ import { control as gonvexControl, type LiveQueryPlan } from "@gonvex/client";
4
4
 
5
5
  export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
6
6
  export type FunctionKind = "query" | "reducer" | "action";
@@ -22,6 +22,27 @@ export type FunctionReference<Kind extends FunctionKind = FunctionKind, Args = J
22
22
  readonly optimistic?: { readonly transaction?: OptimisticTransactionDefinition };
23
23
  };
24
24
 
25
+ export type MessagesAgentInvokeArgs = {
26
+ path: string;
27
+ args: JsonValue;
28
+ artifactHash: string;
29
+ };
30
+ export type MessagesAgentInvokeResult = JsonValue;
31
+ export type MessagesEchoArgs = {
32
+ message: string;
33
+ };
34
+ export type MessagesEchoResult = {
35
+ message: string;
36
+ };
37
+ export type MessagesGetArgs = {
38
+ id: string;
39
+ };
40
+ export type MessagesGetResult = Array<{
41
+ id: string;
42
+ body: string;
43
+ author: string;
44
+ created_at: string;
45
+ }>;
25
46
  export type MessagesListArgs = { };
26
47
  export type MessagesListResult = Array<{
27
48
  id: string;
@@ -41,6 +62,83 @@ export type MessagesSendResult = {
41
62
 
42
63
  export const api = {
43
64
  messages: {
65
+ agentInvoke: {
66
+ args: {
67
+ fields: {
68
+ args: {
69
+ kind: "any",
70
+ },
71
+ artifactHash: {
72
+ kind: "string",
73
+ },
74
+ path: {
75
+ kind: "string",
76
+ },
77
+ },
78
+ kind: "object",
79
+ },
80
+ kind: "action",
81
+ path: "messages.agentInvoke",
82
+ result: {
83
+ kind: "any",
84
+ },
85
+ } as unknown as FunctionReference<"action", MessagesAgentInvokeArgs, MessagesAgentInvokeResult>,
86
+ echo: {
87
+ args: {
88
+ fields: {
89
+ message: {
90
+ kind: "string",
91
+ },
92
+ },
93
+ kind: "object",
94
+ },
95
+ kind: "action",
96
+ path: "messages.echo",
97
+ result: {
98
+ fields: {
99
+ message: {
100
+ kind: "string",
101
+ },
102
+ },
103
+ kind: "object",
104
+ },
105
+ } as unknown as FunctionReference<"action", MessagesEchoArgs, MessagesEchoResult>,
106
+ get: {
107
+ args: {
108
+ fields: {
109
+ id: {
110
+ entity: "messages",
111
+ kind: "id",
112
+ },
113
+ },
114
+ kind: "object",
115
+ },
116
+ delivery: "oneShot",
117
+ kind: "query",
118
+ path: "messages.get",
119
+ result: {
120
+ items: {
121
+ fields: {
122
+ author: {
123
+ kind: "string",
124
+ },
125
+ body: {
126
+ kind: "string",
127
+ },
128
+ created_at: {
129
+ format: "datetime",
130
+ kind: "string",
131
+ },
132
+ id: {
133
+ entity: "messages",
134
+ kind: "id",
135
+ },
136
+ },
137
+ kind: "object",
138
+ },
139
+ kind: "array",
140
+ },
141
+ } as unknown as FunctionReference<"query", MessagesGetArgs, MessagesGetResult>,
44
142
  list: {
45
143
  args: {
46
144
  fields: {
@@ -128,18 +226,27 @@ export const api = {
128
226
  },
129
227
  } as const;
130
228
 
131
- export const internal = {} as const;
229
+ export const control = gonvexControl;
230
+
231
+ export const internal = {
232
+ } as const;
132
233
  export type Api = typeof api;
133
234
 
134
235
  export const optimisticTransactions: Record<string, OptimisticTransactionDefinition> = {
135
236
  };
136
237
 
137
238
  export type ApiArgs = {
239
+ "messages.agentInvoke": MessagesAgentInvokeArgs;
240
+ "messages.echo": MessagesEchoArgs;
241
+ "messages.get": MessagesGetArgs;
138
242
  "messages.list": MessagesListArgs;
139
243
  "messages.send": MessagesSendArgs;
140
244
  };
141
245
 
142
246
  export type ApiResults = {
247
+ "messages.agentInvoke": MessagesAgentInvokeResult;
248
+ "messages.echo": MessagesEchoResult;
249
+ "messages.get": MessagesGetResult;
143
250
  "messages.list": MessagesListResult;
144
251
  "messages.send": MessagesSendResult;
145
252
  };