pgkiln 0.50.0 → 0.52.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.
- package/SECURITY.md +7 -0
- package/db/migrations/119_workspace_home.sql +30 -0
- package/db/migrations/120_datasets.sql +87 -0
- package/db/migrations/121_ai_agents.sql +94 -0
- package/db/migrations/122_team_development.sql +186 -0
- package/db/migrations/123_workflow_outcomes.sql +407 -0
- package/db/migrations/124_component_details.sql +51 -0
- package/docs/apex-feature-parity.md +15 -12
- package/docs/guide/01-installation.md +9 -3
- package/docs/guide/03-builder.md +211 -11
- package/docs/guide/04-pages-and-regions.md +96 -22
- package/docs/guide/05-items.md +7 -1
- package/docs/guide/06-processing.md +123 -8
- package/docs/guide/07-dynamic-actions.md +34 -0
- package/docs/guide/09-reference.md +2 -1
- package/docs/guide/11-from-apex.md +2 -1
- package/docs/guide/12-development.md +70 -11
- package/docs/guide/20-ai-agents.md +4 -2
- package/examples/hr/README.md +2 -0
- package/examples/hr/hr_63_datasets.sql +29 -0
- package/examples/hr/hr_64_ai_agent.sql +97 -0
- package/examples/hr/hr_65_ask_reports.sql +21 -0
- package/examples/hr/hr_66_team_development.sql +58 -0
- package/examples/hr/hr_67_workflow_outcomes.sql +113 -0
- package/examples/hr/hr_68_component_details.sql +96 -0
- package/package.json +1 -1
- package/public/app.css +46 -0
- package/public/app.js +134 -20
- package/public/builder.css +116 -0
- package/public/builder.js +47 -0
- package/src/ai/chat.ts +18 -18
- package/src/ai/service.ts +35 -11
- package/src/ai/types.ts +7 -2
- package/src/app.ts +11 -1
- package/src/builder/advisor.ts +6 -3
- package/src/builder/ai-agents.ts +372 -0
- package/src/builder/ai-builder.ts +3 -3
- package/src/builder/ai.ts +13 -9
- package/src/builder/assistant.ts +22 -7
- package/src/builder/components.ts +38 -9
- package/src/builder/datasets.ts +472 -0
- package/src/builder/designer.ts +8 -3
- package/src/builder/feedback.ts +6 -1
- package/src/builder/home.ts +12 -7
- package/src/builder/instance.ts +2 -0
- package/src/builder/locks.ts +7 -1
- package/src/builder/region-help.ts +2 -2
- package/src/builder/reporter.ts +52 -3
- package/src/builder/routes.ts +7 -4
- package/src/builder/shared.ts +1 -1
- package/src/builder/subscriptions.ts +1 -0
- package/src/builder/team.ts +991 -0
- package/src/builder/ui.ts +8 -5
- package/src/builder/workflow-migrate.ts +210 -0
- package/src/builder/workflows.ts +29 -6
- package/src/builder/workspace-home.ts +285 -0
- package/src/builder/workspace-transfer.ts +1 -1
- package/src/builder/workspaces.ts +7 -4
- package/src/cli/replace.ts +7 -1
- package/src/datasets.ts +367 -0
- package/src/i18n/ar.ts +33 -0
- package/src/i18n/cs.ts +33 -0
- package/src/i18n/da.ts +33 -0
- package/src/i18n/de.ts +33 -0
- package/src/i18n/el.ts +33 -0
- package/src/i18n/es.ts +33 -0
- package/src/i18n/fi.ts +33 -0
- package/src/i18n/fr.ts +33 -0
- package/src/i18n/he.ts +33 -0
- package/src/i18n/it.ts +33 -0
- package/src/i18n/ja.ts +33 -0
- package/src/i18n/ko.ts +33 -0
- package/src/i18n/nb.ts +33 -0
- package/src/i18n/pl.ts +33 -0
- package/src/i18n/pt.ts +33 -0
- package/src/i18n/ru.ts +33 -0
- package/src/i18n/sv.ts +33 -0
- package/src/i18n/tr.ts +33 -0
- package/src/i18n/uk.ts +33 -0
- package/src/i18n/zh.ts +33 -0
- package/src/i18n.ts +66 -0
- package/src/instance.ts +3 -0
- package/src/metadata.ts +9 -3
- package/src/release-check.ts +139 -0
- package/src/reporting-app.ts +101 -0
- package/src/runtime/ai-agents.ts +365 -0
- package/src/runtime/ai-filter.ts +385 -107
- package/src/runtime/assistant.ts +64 -11
- package/src/runtime/context.ts +2 -0
- package/src/runtime/data-reporter.ts +199 -32
- package/src/runtime/ir-phrases.ts +812 -0
- package/src/runtime/ir-plan.ts +449 -0
- package/src/runtime/items.ts +1 -1
- package/src/runtime/regions.ts +6 -2
- package/src/runtime/render.ts +1 -1
- package/src/runtime/report.ts +1 -1
- package/src/runtime/tasks.ts +63 -12
- package/src/session.ts +2 -0
- package/src/subscriptions.ts +2 -1
- package/src/task-events.ts +232 -0
- package/src/team-samples.ts +74 -0
- package/src/team.ts +342 -0
- package/src/workflow.ts +135 -18
- package/src/workspace-export.ts +39 -4
package/src/ai/chat.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import OpenAI from 'openai';
|
|
3
3
|
import { owner } from '../db.ts';
|
|
4
|
-
import { decryptSecret } from '../secrets.ts';
|
|
5
4
|
import { claudeError } from './anthropic.ts';
|
|
6
5
|
import { openAiError } from './openai.ts';
|
|
7
|
-
import {
|
|
6
|
+
import { baseUrlFor, MAX_PROMPT_CHARS, MAX_SYSTEM_CHARS, serviceForApp, serviceKey, usageToday, logUsage, type AiService, type CallInfo } from './service.ts';
|
|
8
7
|
import { AiError } from './types.ts';
|
|
9
8
|
|
|
10
9
|
// Conversations with tools (AI assistant regions, src/runtime/assistant.ts):
|
|
@@ -23,7 +22,9 @@ import { AiError } from './types.ts';
|
|
|
23
22
|
// service's effort, server-side refusal fallbacks. The assistant's content
|
|
24
23
|
// is appended to the history unchanged (thinking blocks included): the
|
|
25
24
|
// history only ever grows, as the API expects. OpenAI: Chat Completions with
|
|
26
|
-
// strict function tools
|
|
25
|
+
// strict function tools; also Gemini (its OpenAI-compatible endpoint) and
|
|
26
|
+
// OpenAI-compatible endpoints (121). An agent (src/runtime/ai-agents.ts)
|
|
27
|
+
// may set the reasoning effort and a lower output limit per call.
|
|
27
28
|
|
|
28
29
|
export interface ChatTool {
|
|
29
30
|
name: string;
|
|
@@ -47,6 +48,10 @@ export interface ChatRequest {
|
|
|
47
48
|
/** at most this many rounds of tool calls (then the model must answer) */
|
|
48
49
|
maxRounds: number;
|
|
49
50
|
runTool(name: string, input: Record<string, unknown>): Promise<ToolOutcome>;
|
|
51
|
+
/** (121) an agent's reasoning effort: Claude's output_config.effort, OpenAI's reasoning_effort (else the service's own) */
|
|
52
|
+
effort?: 'low' | 'medium' | 'high' | null;
|
|
53
|
+
/** (121) an agent's output limit per call (never above the service's maximum) */
|
|
54
|
+
maxTokens?: number | null;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
export interface ChatResult {
|
|
@@ -69,16 +74,9 @@ export interface ChatInfo {
|
|
|
69
74
|
/** Tool results are cut to this many characters (the model sees that they were). */
|
|
70
75
|
export const MAX_TOOL_RESULT_CHARS = 20_000;
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
key = s.api_key_enc ? decryptSecret(s.api_key_enc) : process.env[envKeyName(s.provider)];
|
|
76
|
-
} catch (e) {
|
|
77
|
-
throw new AiError('config', `AI service ${s.name}: ${(e as Error).message}`);
|
|
78
|
-
}
|
|
79
|
-
if (!key) throw new AiError('config', `AI service ${s.name} has no API key (enter one in the builder, or set ${envKeyName(s.provider)} on the server).`);
|
|
80
|
-
return key;
|
|
81
|
-
}
|
|
77
|
+
|
|
78
|
+
/** The output limit of one call: the agent's, never above the service's. */
|
|
79
|
+
const outputLimit = (s: AiService, req: ChatRequest) => Math.min(s.max_tokens, req.maxTokens ?? s.max_tokens);
|
|
82
80
|
|
|
83
81
|
const clip = (text: string) => (text.length > MAX_TOOL_RESULT_CHARS ? `${text.slice(0, MAX_TOOL_RESULT_CHARS)}\n[cut off: the result was longer]` : text);
|
|
84
82
|
|
|
@@ -114,7 +112,7 @@ async function metered<T extends { model: string; input: number; output: number
|
|
|
114
112
|
const TOO_MANY_ROUNDS = 'The assistant needed more steps than it may take for one question: ask a simpler question, or start a new conversation.';
|
|
115
113
|
|
|
116
114
|
async function claudeChat(s: AiService, limits: Parameters<typeof metered>[1], req: ChatRequest, info: ChatInfo): Promise<ChatResult> {
|
|
117
|
-
const client = new Anthropic({ apiKey:
|
|
115
|
+
const client = new Anthropic({ apiKey: serviceKey(s), authToken: null, baseURL: baseUrlFor(s), timeout: s.timeout_s * 1000, maxRetries: 1 });
|
|
118
116
|
const messages = [...(req.history as Anthropic.Beta.BetaMessageParam[]), { role: 'user', content: req.message } as Anthropic.Beta.BetaMessageParam];
|
|
119
117
|
const tools: Anthropic.Beta.BetaTool[] = req.tools.map((t) => ({
|
|
120
118
|
name: t.name, description: t.description, strict: true, input_schema: t.parameters as Anthropic.Beta.BetaTool.InputSchema,
|
|
@@ -126,11 +124,11 @@ async function claudeChat(s: AiService, limits: Parameters<typeof metered>[1], r
|
|
|
126
124
|
try {
|
|
127
125
|
m = await client.beta.messages.stream({
|
|
128
126
|
model: s.model,
|
|
129
|
-
max_tokens: s
|
|
127
|
+
max_tokens: outputLimit(s, req),
|
|
130
128
|
system: req.system,
|
|
131
129
|
messages,
|
|
132
130
|
...(tools.length ? { tools } : {}),
|
|
133
|
-
...(s.effort ? { output_config: { effort: s.effort } } : {}),
|
|
131
|
+
...((req.effort ?? s.effort) ? { output_config: { effort: (req.effort ?? s.effort)! } } : {}),
|
|
134
132
|
...(s.refusal_fallback ? { betas: ['server-side-fallback-2026-07-01'], fallbacks: 'default' as const } : {}),
|
|
135
133
|
}).finalMessage();
|
|
136
134
|
} catch (e) {
|
|
@@ -161,7 +159,7 @@ async function claudeChat(s: AiService, limits: Parameters<typeof metered>[1], r
|
|
|
161
159
|
}
|
|
162
160
|
|
|
163
161
|
async function openAiChat(s: AiService, limits: Parameters<typeof metered>[1], req: ChatRequest, info: ChatInfo): Promise<ChatResult> {
|
|
164
|
-
const client = new OpenAI({ apiKey:
|
|
162
|
+
const client = new OpenAI({ apiKey: serviceKey(s), baseURL: baseUrlFor(s), timeout: s.timeout_s * 1000, maxRetries: 1 });
|
|
165
163
|
const messages = [...(req.history as OpenAI.Chat.ChatCompletionMessageParam[]), { role: 'user', content: req.message } as OpenAI.Chat.ChatCompletionMessageParam];
|
|
166
164
|
const tools: OpenAI.Chat.ChatCompletionTool[] = req.tools.map((t) => ({
|
|
167
165
|
type: 'function', function: { name: t.name, description: t.description, parameters: t.parameters, strict: true },
|
|
@@ -175,8 +173,10 @@ async function openAiChat(s: AiService, limits: Parameters<typeof metered>[1], r
|
|
|
175
173
|
model: s.model,
|
|
176
174
|
// the system prompt is sent with each request, not kept in the history
|
|
177
175
|
messages: [{ role: 'system', content: req.system }, ...messages],
|
|
178
|
-
max_completion_tokens: s
|
|
176
|
+
max_completion_tokens: outputLimit(s, req),
|
|
179
177
|
...(tools.length ? { tools } : {}),
|
|
178
|
+
// (121) an agent's reasoning effort; models without reasoning refuse it, so none is sent without one
|
|
179
|
+
...(req.effort ? { reasoning_effort: req.effort } : {}),
|
|
180
180
|
});
|
|
181
181
|
} catch (e) {
|
|
182
182
|
throw openAiError(e);
|
package/src/ai/service.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { ClaudeProvider } from './anthropic.ts';
|
|
|
4
4
|
import { OpenAiProvider } from './openai.ts';
|
|
5
5
|
import { AiError, type AiProvider, type GenerateRequest, type GenerateResult, type ProviderName } from './types.ts';
|
|
6
6
|
|
|
7
|
-
// AI services (migration 060): an administrator configures them in the
|
|
7
|
+
// AI services (migration 060; Gemini and OpenAI-compatible endpoints: 121): an administrator configures them in the
|
|
8
8
|
// builder (Workspace utilities → AI services) and allows each application
|
|
9
9
|
// to use some of them, with optional limits per day. Every call goes
|
|
10
10
|
// through generate() here: the service must exist, be enabled and be
|
|
@@ -35,33 +35,57 @@ export const DEFAULT_CLAUDE_MODEL = 'claude-opus-5-5';
|
|
|
35
35
|
export const MAX_PROMPT_CHARS = 200_000;
|
|
36
36
|
export const MAX_SYSTEM_CHARS = 50_000;
|
|
37
37
|
|
|
38
|
-
const ENV_KEY: Record<ProviderName, string> = {
|
|
38
|
+
const ENV_KEY: Record<ProviderName, string> = {
|
|
39
|
+
anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY', gemini: 'GEMINI_API_KEY', openai_compatible: 'OPENAI_COMPATIBLE_API_KEY',
|
|
40
|
+
};
|
|
39
41
|
|
|
40
42
|
export const envKeyName = (p: ProviderName) => ENV_KEY[p];
|
|
41
43
|
|
|
44
|
+
/** (121) The providers, as the builder names them. */
|
|
45
|
+
export const PROVIDER_LABELS: Record<ProviderName, string> = {
|
|
46
|
+
anthropic: 'Claude', openai: 'OpenAI', gemini: 'Gemini', openai_compatible: 'OpenAI-compatible',
|
|
47
|
+
};
|
|
48
|
+
export const providerLabel = (p: string) => PROVIDER_LABELS[p as ProviderName] ?? p;
|
|
49
|
+
|
|
50
|
+
/** (121) Google's OpenAI-compatible endpoint for Gemini (used when a Gemini service names no base URL). */
|
|
51
|
+
export const GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/openai/';
|
|
52
|
+
|
|
53
|
+
/** The base URL a service's SDK client uses: its own, Gemini's default, or the SDK's default (undefined). */
|
|
54
|
+
export const baseUrlFor = (s: Pick<AiService, 'provider' | 'base_url'>) => s.base_url || (s.provider === 'gemini' ? GEMINI_BASE_URL : undefined);
|
|
55
|
+
|
|
42
56
|
/** Where the service's key comes from: stored (encrypted), the server's environment, or nowhere. */
|
|
43
57
|
export function keySource(s: Pick<AiService, 'provider' | 'api_key_enc'>): 'stored' | 'env' | 'missing' {
|
|
44
58
|
if (s.api_key_enc) return 'stored';
|
|
45
59
|
return process.env[ENV_KEY[s.provider]] ? 'env' : 'missing';
|
|
46
60
|
}
|
|
47
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The service's API key, decrypted only here (never stored elsewhere): the stored one, else the server's
|
|
64
|
+
* environment variable. An OpenAI-compatible endpoint may need none (a local Ollama): the SDK then sends a
|
|
65
|
+
* placeholder.
|
|
66
|
+
*/
|
|
67
|
+
export function serviceKey(s: AiService): string {
|
|
68
|
+
let key: string | undefined;
|
|
69
|
+
try {
|
|
70
|
+
key = s.api_key_enc ? decryptSecret(s.api_key_enc) : process.env[ENV_KEY[s.provider]];
|
|
71
|
+
} catch (e) {
|
|
72
|
+
throw new AiError('config', `AI service ${s.name}: ${(e as Error).message}`);
|
|
73
|
+
}
|
|
74
|
+
if (!key && s.provider === 'openai_compatible') return 'none';
|
|
75
|
+
if (!key) throw new AiError('config', `AI service ${s.name} has no API key (enter one in the builder, or set ${ENV_KEY[s.provider]} on the server).`);
|
|
76
|
+
return key;
|
|
77
|
+
}
|
|
78
|
+
|
|
48
79
|
export async function loadService(name: string): Promise<AiService | undefined> {
|
|
49
80
|
return owner.one<AiService>('select * from meta.ai_service where name = $1', [name.toUpperCase()]);
|
|
50
81
|
}
|
|
51
82
|
|
|
52
83
|
/** The provider of a service, with its key (decrypted only here, never stored elsewhere). */
|
|
53
84
|
export function providerFor(s: AiService): AiProvider {
|
|
54
|
-
let apiKey: string | undefined;
|
|
55
|
-
try {
|
|
56
|
-
apiKey = s.api_key_enc ? decryptSecret(s.api_key_enc) : process.env[ENV_KEY[s.provider]];
|
|
57
|
-
} catch (e) {
|
|
58
|
-
throw new AiError('config', `AI service ${s.name}: ${(e as Error).message}`);
|
|
59
|
-
}
|
|
60
|
-
if (!apiKey) throw new AiError('config', `AI service ${s.name} has no API key (enter one in the builder, or set ${ENV_KEY[s.provider]} on the server).`);
|
|
61
85
|
const conf = {
|
|
62
86
|
model: s.model,
|
|
63
|
-
apiKey,
|
|
64
|
-
baseUrl: s
|
|
87
|
+
apiKey: serviceKey(s),
|
|
88
|
+
baseUrl: baseUrlFor(s),
|
|
65
89
|
timeoutMs: s.timeout_s * 1000,
|
|
66
90
|
maxTokens: s.max_tokens,
|
|
67
91
|
effort: s.effort,
|
package/src/ai/types.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// The provider interface of pgkiln's AI features (src/ai/): one request in,
|
|
2
2
|
// one answer out, whatever the provider. Providers live in anthropic.ts
|
|
3
3
|
// (Claude, with the official @anthropic-ai/sdk) and openai.ts (the official
|
|
4
|
-
// openai SDK
|
|
4
|
+
// openai SDK: OpenAI itself, Google Gemini through its OpenAI-compatible
|
|
5
|
+
// endpoint, and any OpenAI-compatible endpoint such as Ollama, vLLM or Azure
|
|
6
|
+
// OpenAI); service.ts picks one for an AI service and adds the checks,
|
|
5
7
|
// limits and the usage log.
|
|
6
8
|
|
|
7
|
-
export type ProviderName = 'anthropic' | 'openai';
|
|
9
|
+
export type ProviderName = 'anthropic' | 'openai' | 'gemini' | 'openai_compatible';
|
|
10
|
+
|
|
11
|
+
/** Providers that speak OpenAI's Chat Completions (the openai SDK). */
|
|
12
|
+
export const isOpenAiFamily = (p: ProviderName) => p !== 'anthropic';
|
|
8
13
|
|
|
9
14
|
export interface ProviderConfig {
|
|
10
15
|
/** the model, exactly as the developer entered it (never changed or downgraded) */
|
package/src/app.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { webSourceRoutes } from './builder/websources.ts';
|
|
|
16
16
|
import { sqlRoutes } from './builder/sql.ts';
|
|
17
17
|
import { automationRoutes } from './builder/automations.ts';
|
|
18
18
|
import { workflowBuilderRoutes } from './builder/workflows.ts';
|
|
19
|
+
import { workflowMigrateRoutes } from './builder/workflow-migrate.ts';
|
|
19
20
|
import { regionSettingsRoutes } from './builder/region-settings.ts';
|
|
20
21
|
import { templateRoutes } from './builder/templates.ts';
|
|
21
22
|
import { searchRoutes } from './builder/search.ts';
|
|
@@ -24,6 +25,7 @@ import { topSqlRoutes } from './builder/top-sql.ts';
|
|
|
24
25
|
import { diagnosticsRoutes } from './builder/diagnostics.ts';
|
|
25
26
|
import { ldapRoutes } from './builder/ldap.ts';
|
|
26
27
|
import { aiRoutes } from './builder/ai.ts';
|
|
28
|
+
import { aiAgentRoutes } from './builder/ai-agents.ts';
|
|
27
29
|
import { mailRoutes } from './builder/mail.ts';
|
|
28
30
|
import { backupRoutes } from './builder/backups.ts';
|
|
29
31
|
import { availabilityRoutes } from './builder/availability.ts';
|
|
@@ -52,7 +54,10 @@ import { blueprintRoutes } from './builder/blueprints.ts';
|
|
|
52
54
|
import { accountRoutes } from './runtime/account.ts';
|
|
53
55
|
import { taskRoutes } from './runtime/tasks.ts';
|
|
54
56
|
import { dataReporterRoutes } from './runtime/data-reporter.ts';
|
|
57
|
+
import { datasetRoutes } from './builder/datasets.ts';
|
|
55
58
|
import { reporterBuilderRoutes } from './builder/reporter.ts';
|
|
59
|
+
import { workspaceHomeRoutes } from './builder/workspace-home.ts';
|
|
60
|
+
import { teamRoutes } from './builder/team.ts';
|
|
56
61
|
import { formDesignerRoutes } from './runtime/form-designer.ts';
|
|
57
62
|
import { formDesignerBuilderRoutes } from './builder/form-designer.ts';
|
|
58
63
|
import { builderErrorPages, builderServerError } from './builder/errors.ts';
|
|
@@ -213,6 +218,7 @@ export async function buildApp(opts: { logger?: boolean } = {}) {
|
|
|
213
218
|
await app.register(layoutRoutes);
|
|
214
219
|
await app.register(automationRoutes);
|
|
215
220
|
await app.register(workflowBuilderRoutes);
|
|
221
|
+
await app.register(workflowMigrateRoutes);
|
|
216
222
|
await app.register(reportSettingsRoutes);
|
|
217
223
|
await app.register(regionSettingsRoutes);
|
|
218
224
|
await app.register(templateRoutes);
|
|
@@ -223,6 +229,7 @@ export async function buildApp(opts: { logger?: boolean } = {}) {
|
|
|
223
229
|
await app.register(diagnosticsRoutes);
|
|
224
230
|
await app.register(ldapRoutes);
|
|
225
231
|
await app.register(aiRoutes);
|
|
232
|
+
await app.register(aiAgentRoutes);
|
|
226
233
|
await app.register(mailRoutes);
|
|
227
234
|
await app.register(backupRoutes);
|
|
228
235
|
await app.register(availabilityRoutes);
|
|
@@ -239,6 +246,7 @@ export async function buildApp(opts: { logger?: boolean } = {}) {
|
|
|
239
246
|
await app.register(codeEditorRoutes);
|
|
240
247
|
await app.register(dataReporterRoutes);
|
|
241
248
|
await app.register(reporterBuilderRoutes);
|
|
249
|
+
await app.register(datasetRoutes);
|
|
242
250
|
await app.register(formDesignerRoutes);
|
|
243
251
|
await app.register(formDesignerBuilderRoutes);
|
|
244
252
|
await app.register(sampleDataRoutes);
|
|
@@ -247,6 +255,8 @@ export async function buildApp(opts: { logger?: boolean } = {}) {
|
|
|
247
255
|
await app.register(assistantBuilderRoutes);
|
|
248
256
|
await app.register(aiBuilderRoutes);
|
|
249
257
|
await app.register(blueprintRoutes);
|
|
250
|
-
app.
|
|
258
|
+
await app.register(workspaceHomeRoutes);
|
|
259
|
+
await app.register(teamRoutes);
|
|
260
|
+
app.get('/', async (_req, reply) => reply.redirect('/builder/home'));
|
|
251
261
|
return app;
|
|
252
262
|
}
|
package/src/builder/advisor.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { devDbForApp } from '../workspace-db.ts';
|
|
|
6
6
|
import { html, type Raw } from '../html.ts';
|
|
7
7
|
import { icon } from '../icons.ts';
|
|
8
8
|
import { templateProblem } from '../runtime/document.ts';
|
|
9
|
-
import { invokeStepReferences, stepProblems, stepWarnings } from '../workflow.ts';
|
|
9
|
+
import { invokeStepReferences, isQuery, outcomeMap, stepProblems, stepWarnings } from '../workflow.ts';
|
|
10
10
|
import { handlerProblems } from '../runtime/rest.ts';
|
|
11
11
|
import { plpgsqlBlock } from '../runtime/rest-handlers.ts';
|
|
12
12
|
import { processProblems } from '../runtime/processes.ts';
|
|
@@ -272,6 +272,8 @@ export async function advise(appId: number, tx?: pg.ClientBase, opts: { db?: Db
|
|
|
272
272
|
if (st?.type === 'sql' && typeof st.code === 'string') checks.push([`${v}step ${st.name}: code`, st.code, 'statements']);
|
|
273
273
|
if (st?.type === 'task' && typeof st.owners === 'string' && st.owners.trim()) checks.push([`${v}step ${st.name}: owners`, st.owners, 'select']);
|
|
274
274
|
if (st?.type === 'switch' && Array.isArray(st.cases)) for (const cs of st.cases) if (typeof cs?.when === 'string') checks.push([`${v}step ${st.name}: when`, cs.when, 'boolean']);
|
|
275
|
+
// (123) a send_mail step's recipients query
|
|
276
|
+
if (st?.type === 'send_mail' && typeof st.to === 'string' && isQuery(st.to)) checks.push([`${v}step ${st.name}: to`, st.to, 'select']);
|
|
275
277
|
for (const [field, sql, shape] of checks) {
|
|
276
278
|
checked++;
|
|
277
279
|
const problem = await checkSql(c, sql, shape);
|
|
@@ -369,7 +371,8 @@ export async function advise(appId: number, tx?: pg.ClientBase, opts: { db?: Db
|
|
|
369
371
|
if (kind === 'rest_module') for (const problem of handlerProblems(row.handlers)) missing(e, 'Handlers (JSON)', problem);
|
|
370
372
|
if (kind === 'workflow_definition') {
|
|
371
373
|
const tasks = new Set(all.filter((x) => x.kind === 'task_definition').map((x) => x.row.name.toUpperCase()));
|
|
372
|
-
|
|
374
|
+
const outcomes = outcomeMap(all.filter((x) => x.kind === 'task_definition').map((x) => x.row));
|
|
375
|
+
for (const problem of stepProblems(row.steps, tasks, { outcomes, drafts: false })) missing(e, 'Steps (JSON)', problem);
|
|
373
376
|
for (const warning of stepWarnings(row.steps)) missing(e, 'Steps (JSON)', warning, 'warning');
|
|
374
377
|
// invoke_api steps: REST data sources, parameters, web credentials, variables (sprint 32)
|
|
375
378
|
const refs = {
|
|
@@ -382,7 +385,7 @@ export async function advise(appId: number, tx?: pg.ClientBase, opts: { db?: Db
|
|
|
382
385
|
for (const warning of invokeRefs.warnings) missing(e, 'Steps (JSON)', warning, 'warning');
|
|
383
386
|
if (row.dev_version) {
|
|
384
387
|
const field = `Steps of version ${row.dev_version} (development)`;
|
|
385
|
-
for (const problem of stepProblems(row.dev_steps, tasks)) missing(e, field, problem, 'warning');
|
|
388
|
+
for (const problem of stepProblems(row.dev_steps, tasks, { outcomes })) missing(e, field, problem, 'warning');
|
|
386
389
|
for (const warning of stepWarnings(row.dev_steps)) missing(e, field, warning, 'warning');
|
|
387
390
|
const devRefs = invokeStepReferences(row.dev_steps, refs);
|
|
388
391
|
for (const problem of [...devRefs.errors, ...devRefs.warnings]) missing(e, field, problem, 'warning');
|