@pikku/cli 0.12.125 → 0.12.126
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/CHANGELOG.md +166 -0
- package/dist/.pikku/addon/index.d.ts +1 -1
- package/dist/.pikku/addon/index.js +1 -1
- package/dist/.pikku/addon/pikku-addon-types.gen.d.ts +1 -1
- package/dist/.pikku/addon/pikku-addon-types.gen.js +1 -1
- package/dist/.pikku/agent/index.d.ts +1 -1
- package/dist/.pikku/agent/index.js +1 -1
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/agent/pikku-model-aliases.gen.js +1 -1
- package/dist/.pikku/auth/index.d.ts +1 -1
- package/dist/.pikku/auth/index.js +1 -1
- package/dist/.pikku/auth/pikku-auth-types.gen.d.ts +1 -1
- package/dist/.pikku/auth/pikku-auth-types.gen.js +1 -1
- package/dist/.pikku/channel/index.d.ts +1 -1
- package/dist/.pikku/channel/index.js +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/index.d.ts +1 -1
- package/dist/.pikku/cli/index.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +6 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +13 -0
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/error/index.d.ts +1 -1
- package/dist/.pikku/error/index.js +1 -1
- package/dist/.pikku/error/pikku-error-types.gen.d.ts +1 -1
- package/dist/.pikku/error/pikku-error-types.gen.js +1 -1
- package/dist/.pikku/function/index.d.ts +1 -1
- package/dist/.pikku/function/index.js +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +35 -18
- package/dist/.pikku/function/pikku-functions.gen.js +1 -1
- package/dist/.pikku/gateway/index.d.ts +1 -1
- package/dist/.pikku/gateway/index.js +1 -1
- package/dist/.pikku/gateway/pikku-gateway-types.gen.d.ts +1 -1
- package/dist/.pikku/gateway/pikku-gateway-types.gen.js +1 -1
- package/dist/.pikku/http/index.d.ts +1 -1
- package/dist/.pikku/http/index.js +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/mcp/index.d.ts +1 -1
- package/dist/.pikku/mcp/index.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/middleware/index.d.ts +1 -1
- package/dist/.pikku/middleware/index.js +1 -1
- package/dist/.pikku/middleware/pikku-middleware-types.gen.d.ts +1 -1
- package/dist/.pikku/middleware/pikku-middleware-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +1 -1
- package/dist/.pikku/queue/index.d.ts +1 -1
- package/dist/.pikku/queue/index.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +1 -0
- package/dist/.pikku/scenarios/index.d.ts +1 -1
- package/dist/.pikku/scenarios/index.js +1 -1
- package/dist/.pikku/scenarios/pikku-personas.gen.js +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-types.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/schemas/register.gen.d.ts +1 -1
- package/dist/.pikku/scenarios/schemas/register.gen.js +1 -1
- package/dist/.pikku/scheduler/index.d.ts +1 -1
- package/dist/.pikku/scheduler/index.js +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +3 -1
- package/dist/.pikku/schemas/schemas/FabricSecretsListInput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/FabricSecretsRotateInput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/FabricSecretsSetInput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/FabricVariablesGetInput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/FabricVariablesSetInput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/PersonaSecretInput.schema.json +1 -0
- package/dist/.pikku/scopes/index.d.ts +1 -1
- package/dist/.pikku/scopes/index.js +1 -1
- package/dist/.pikku/scopes/pikku-personas.gen.js +1 -1
- package/dist/.pikku/scopes/pikku-roles.gen.d.ts +1 -1
- package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
- package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
- package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
- package/dist/.pikku/secrets/index.d.ts +1 -1
- package/dist/.pikku/secrets/index.js +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/services/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/services/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/setup/index.d.ts +1 -1
- package/dist/.pikku/setup/index.js +1 -1
- package/dist/.pikku/setup/pikku-setup-types.gen.d.ts +1 -1
- package/dist/.pikku/setup/pikku-setup-types.gen.js +1 -1
- package/dist/.pikku/trigger/index.d.ts +1 -1
- package/dist/.pikku/trigger/index.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/index.d.ts +1 -1
- package/dist/.pikku/variables/index.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/index.d.ts +1 -1
- package/dist/.pikku/workflow/index.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/cli.wiring.js +6 -1
- package/dist/src/deploy/bundler/bun-bundler.js +12 -5
- package/dist/src/fabric/fabric-commands.d.ts +15 -15
- package/dist/src/fabric/functions/deploy.function.js +12 -2
- package/dist/src/fabric/functions/secrets-list.function.d.ts +4 -4
- package/dist/src/fabric/functions/secrets-list.function.js +4 -4
- package/dist/src/fabric/functions/secrets-rotate.function.d.ts +4 -4
- package/dist/src/fabric/functions/secrets-rotate.function.js +8 -5
- package/dist/src/fabric/functions/secrets-set.function.d.ts +4 -4
- package/dist/src/fabric/functions/secrets-set.function.js +4 -4
- package/dist/src/fabric/functions/validate.function.js +49 -24
- package/dist/src/fabric/functions/variables-get.function.d.ts +4 -4
- package/dist/src/fabric/functions/variables-get.function.js +4 -4
- package/dist/src/fabric/functions/variables-set.function.d.ts +4 -4
- package/dist/src/fabric/functions/variables-set.function.js +4 -4
- package/dist/src/fabric/lib/blank-comments.d.ts +21 -0
- package/dist/src/fabric/lib/blank-comments.js +71 -0
- package/dist/src/fabric/lib/blank-scenario-meta.d.ts +1 -0
- package/dist/src/fabric/lib/blank-scenario-meta.js +110 -0
- package/dist/src/fabric/lib/deployment.d.ts +26 -0
- package/dist/src/fabric/lib/deployment.js +35 -0
- package/dist/src/fabric/lib/stage.d.ts +19 -3
- package/dist/src/fabric/lib/stage.js +29 -6
- package/dist/src/functions/commands/db-reset.js +6 -2
- package/dist/src/functions/commands/dev.js +5 -1
- package/dist/src/functions/commands/persona.d.ts +18 -0
- package/dist/src/functions/commands/persona.js +41 -2
- package/dist/src/functions/commands/scenario-browser.d.ts +13 -5
- package/dist/src/functions/db/check-enums.d.ts +38 -0
- package/dist/src/functions/db/check-enums.js +103 -0
- package/dist/src/functions/db/db-codegen.js +15 -0
- package/dist/src/functions/db/postgres/postgres-introspector.d.ts +14 -0
- package/dist/src/functions/db/postgres/postgres-introspector.js +48 -0
- package/dist/src/functions/db/sqlite/sqlite-introspector.d.ts +3 -4
- package/dist/src/functions/db/sqlite/sqlite-introspector.js +5 -17
- package/dist/src/functions/wirings/virtual-user/serialize-virtual-user-functions.d.ts +9 -6
- package/dist/src/functions/wirings/virtual-user/serialize-virtual-user-functions.js +51 -27
- package/dist/src/server/actor-sign-in.d.ts +24 -1
- package/dist/src/server/actor-sign-in.js +45 -2
- package/dist/src/utils/persona-credentials.d.ts +23 -2
- package/dist/src/utils/persona-credentials.js +55 -2
- package/dist/src/utils/pikku-cli-config.d.ts +4 -4
- package/dist/src/utils/pikku-cli-config.js +30 -10
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/surface.json +101 -51
|
@@ -43,13 +43,17 @@ export const dbReset = pikkuSessionlessFunc({
|
|
|
43
43
|
logger.info(zod.written
|
|
44
44
|
? `db reset: regenerated ${zod.outFile} (${zod.tables.length} tables)`
|
|
45
45
|
: `db reset: ${zod.outFile} unchanged`);
|
|
46
|
+
// Both lines report on the SEED STEP, never on the database. A migration
|
|
47
|
+
// is free to carry rows — data a deployed stage needs is migration data —
|
|
48
|
+
// so "database is empty" was a verdict this step has no standing to reach,
|
|
49
|
+
// and it read as a failed reset every time it was wrong.
|
|
46
50
|
if (noSeed) {
|
|
47
|
-
logger.info('db reset: --no-seed,
|
|
51
|
+
logger.info('db reset: --no-seed, skipping the dev seed');
|
|
48
52
|
return;
|
|
49
53
|
}
|
|
50
54
|
const devSeedResult = await devSeed(resolved);
|
|
51
55
|
logger.info(devSeedResult.applied
|
|
52
56
|
? `db reset: seeded ${resolved.devSeedFile} (${devSeedResult.bytes} bytes)`
|
|
53
|
-
: `db reset: no ${resolved.devSeedFile} found
|
|
57
|
+
: `db reset: no dev seed applied (${resolved.devSeedFile} not found)`);
|
|
54
58
|
},
|
|
55
59
|
});
|
|
@@ -24,13 +24,17 @@ import { resolveConsoleMount } from './serve-console.js';
|
|
|
24
24
|
import { serverReadyLine } from '../../server/server-ready.js';
|
|
25
25
|
import { createEphemeralContentSigningJWT } from '../../server/content-signing-jwt.js';
|
|
26
26
|
import { enableDevActorSignIn } from '../../server/actor-sign-in.js';
|
|
27
|
+
import { resolvePersonas } from '../../utils/resolve-personas.js';
|
|
27
28
|
import { applyModelAliasOverride } from '../../utils/model-alias-override.js';
|
|
28
29
|
export const dev = pikkuSessionlessFunc({
|
|
29
30
|
remote: true,
|
|
30
31
|
func: async ({ logger, config, getInspectorState, invalidateInspectorState, variables, devServerRunner, }, { port, watch, hmr, coverage, test, model }, { rpc }) => {
|
|
31
32
|
process.env.PIKKU_DEV_QUICK_LOGIN ??= 'true';
|
|
32
33
|
process.env.PIKKU_ENV ??= resolveDevEnvironmentName(config.environments ?? {});
|
|
33
|
-
enableDevActorSignIn(logger)
|
|
34
|
+
await enableDevActorSignIn(logger, async () => {
|
|
35
|
+
const state = await getInspectorState(true, false, false, true);
|
|
36
|
+
return Object.values(resolvePersonas(state.personas?.definitions ?? [], config.scenarios?.emailDomain));
|
|
37
|
+
});
|
|
34
38
|
applyModelAliasOverride(logger, model, config.models);
|
|
35
39
|
if (test) {
|
|
36
40
|
process.env.PIKKU_TEST_RUN = 'true';
|
|
@@ -55,4 +55,22 @@ export declare const personaRun: import("#pikku/function").PikkuFunctionConfig<{
|
|
|
55
55
|
out?: string;
|
|
56
56
|
}, void, "rpc" | "session", import("#pikku/function").WiredServices>, undefined, undefined>;
|
|
57
57
|
/** Who this app is for, and what each of them wants, without running anything. */
|
|
58
|
+
/**
|
|
59
|
+
* Mint the actor credential for one persona, or for every declared persona.
|
|
60
|
+
*
|
|
61
|
+
* The root secret is entitled to all of them, so nothing that only needs to be
|
|
62
|
+
* one person should ever hold it. What comes out is bound to that persona's
|
|
63
|
+
* address by the target — presenting it for anyone else is refused — so a CI
|
|
64
|
+
* job, a container or a virtual user can be handed exactly the people it is
|
|
65
|
+
* meant to be and no more.
|
|
66
|
+
*
|
|
67
|
+
* Printed as `id=secret` lines, which is the shape PIKKU_PERSONA_SECRETS reads.
|
|
68
|
+
*/
|
|
69
|
+
export declare const personaSecret: import("#pikku/function").PikkuFunctionConfig<{
|
|
70
|
+
personas?: string[];
|
|
71
|
+
}, void, "rpc" | "session", import("#pikku/function").PikkuFunctionSessionless<{
|
|
72
|
+
personas?: string[];
|
|
73
|
+
}, void, "rpc" | "session", import("#pikku/function").WiredServices, unknown> | import("#pikku/function").PikkuFunction<{
|
|
74
|
+
personas?: string[];
|
|
75
|
+
}, void, "rpc" | "session", import("#pikku/function").WiredServices>, undefined, undefined>;
|
|
58
76
|
export declare const personaList: import("#pikku/function").PikkuFunctionConfig<void, void, "rpc" | "session", import("#pikku/function").PikkuFunctionSessionless<void, void, "rpc" | "session", import("#pikku/function").WiredServices, unknown> | import("#pikku/function").PikkuFunction<void, void, "rpc" | "session", import("#pikku/function").WiredServices>, undefined, undefined>;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { resolve, dirname } from 'node:path';
|
|
2
2
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { pikkuSessionlessFunc } from '#pikku/function';
|
|
4
|
-
import { createHttpPersonas, personaEnvironmentRefusal, roleMismatchMessage, verifyPersonaRoles, } from '@pikku/core/persona';
|
|
4
|
+
import { createHttpPersonas, deriveActorSecret, personaEnvironmentRefusal, roleMismatchMessage, verifyPersonaRoles, } from '@pikku/core/persona';
|
|
5
5
|
import { personaVirtualUserTarget, runVirtualUser, catalogueClassification, DISPOSITIONS, prepareVirtualUserRun, } from '@pikku/core/virtual-user';
|
|
6
6
|
import { getSingletonServices, setSingletonServices } from '@pikku/core/state';
|
|
7
7
|
import { resolvePersonas } from '../../utils/resolve-personas.js';
|
|
8
|
-
import { resolvePersonaCredentials } from '../../utils/persona-credentials.js';
|
|
8
|
+
import { ACTOR_SECRET_VARIABLE, PERSONA_SECRETS_VARIABLE, resolvePersonaCredentials, } from '../../utils/persona-credentials.js';
|
|
9
9
|
import { resolveEnvironment } from './environment.js';
|
|
10
10
|
import { createDevAgentRunner } from './dev-agent-runner.js';
|
|
11
11
|
import { formatVirtualUserReport } from './virtual-user-formatter.js';
|
|
@@ -191,6 +191,45 @@ export const personaRun = pikkuSessionlessFunc({
|
|
|
191
191
|
},
|
|
192
192
|
});
|
|
193
193
|
/** Who this app is for, and what each of them wants, without running anything. */
|
|
194
|
+
/**
|
|
195
|
+
* Mint the actor credential for one persona, or for every declared persona.
|
|
196
|
+
*
|
|
197
|
+
* The root secret is entitled to all of them, so nothing that only needs to be
|
|
198
|
+
* one person should ever hold it. What comes out is bound to that persona's
|
|
199
|
+
* address by the target — presenting it for anyone else is refused — so a CI
|
|
200
|
+
* job, a container or a virtual user can be handed exactly the people it is
|
|
201
|
+
* meant to be and no more.
|
|
202
|
+
*
|
|
203
|
+
* Printed as `id=secret` lines, which is the shape PIKKU_PERSONA_SECRETS reads.
|
|
204
|
+
*/
|
|
205
|
+
export const personaSecret = pikkuSessionlessFunc({
|
|
206
|
+
func: async ({ logger, config, variables, getInspectorState }, input) => {
|
|
207
|
+
const root = await variables.get(ACTOR_SECRET_VARIABLE);
|
|
208
|
+
if (!root) {
|
|
209
|
+
throw new Error(`${ACTOR_SECRET_VARIABLE} is not set — there is no root to derive from. ` +
|
|
210
|
+
`It lives in the environment of whoever mints credentials, and never in pikku.config.json.`);
|
|
211
|
+
}
|
|
212
|
+
const state = await getInspectorState(true, false, false, true);
|
|
213
|
+
const declared = resolvePersonas(state.personas?.definitions ?? [], config.scenarios?.emailDomain);
|
|
214
|
+
const wanted = input.personas?.length
|
|
215
|
+
? input.personas
|
|
216
|
+
: Object.keys(declared);
|
|
217
|
+
const unknown = wanted.filter((id) => !declared[id]);
|
|
218
|
+
if (unknown.length > 0) {
|
|
219
|
+
throw new Error(`No persona ${unknown.map((id) => `'${id}'`).join(', ')} is declared. ` +
|
|
220
|
+
`Declared: ${Object.keys(declared).sort().join(', ') || 'none'}.`);
|
|
221
|
+
}
|
|
222
|
+
const minted = [];
|
|
223
|
+
for (const id of wanted) {
|
|
224
|
+
minted.push(`${id}=${await deriveActorSecret(String(root), declared[id].email)}`);
|
|
225
|
+
}
|
|
226
|
+
for (const line of minted) {
|
|
227
|
+
logger.info(line);
|
|
228
|
+
}
|
|
229
|
+
logger.info('');
|
|
230
|
+
logger.info(`Hand these to a run as ${PERSONA_SECRETS_VARIABLE}='${minted.join(',')}'`);
|
|
231
|
+
},
|
|
232
|
+
});
|
|
194
233
|
export const personaList = pikkuSessionlessFunc({
|
|
195
234
|
func: async ({ logger, config, getInspectorState }) => {
|
|
196
235
|
const state = await getInspectorState(true, false, false, true);
|
|
@@ -9,12 +9,16 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { ScenarioArtifact, ScenarioBrowserFailure, ScenarioBrowserProvider } from '@pikku/core/scenario';
|
|
11
11
|
import type { ResolvedPersona } from '@pikku/core/services';
|
|
12
|
-
import type { OperatorSignInOptions } from '@pikku/core/persona';
|
|
12
|
+
import type { ActorSecretResolver, OperatorSignInOptions } from '@pikku/core/persona';
|
|
13
13
|
/** The default driver, used when a project names no other. */
|
|
14
14
|
export declare const DEFAULT_BROWSER_DRIVER = "@pikku/playwright";
|
|
15
15
|
export interface ScenarioBrowserDriverOptions {
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The root actor secret, for a local `pikku dev` target — each persona's
|
|
18
|
+
* credential is derived from it. A resolver instead when this run was handed
|
|
19
|
+
* credentials for only some personas.
|
|
20
|
+
*/
|
|
21
|
+
secret?: string | ActorSecretResolver;
|
|
18
22
|
/** Fabric operator credentials, for a deployed target. */
|
|
19
23
|
operator?: OperatorSignInOptions;
|
|
20
24
|
actors: Record<string, ResolvedPersona>;
|
|
@@ -72,8 +76,12 @@ export interface ResolveScenarioBrowserProviderOptions {
|
|
|
72
76
|
appUrl?: string;
|
|
73
77
|
/** Base url per app, for a product whose personas sign into more than one. */
|
|
74
78
|
appUrls?: Record<string, string>;
|
|
75
|
-
/**
|
|
76
|
-
|
|
79
|
+
/**
|
|
80
|
+
* The root actor secret, for a local `pikku dev` target — each persona's
|
|
81
|
+
* credential is derived from it. A resolver instead when this run was handed
|
|
82
|
+
* credentials for only some personas.
|
|
83
|
+
*/
|
|
84
|
+
secret?: string | ActorSecretResolver;
|
|
77
85
|
/** Fabric operator credentials, for a deployed target. */
|
|
78
86
|
operator?: OperatorSignInOptions;
|
|
79
87
|
actors: Record<string, ResolvedPersona>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `CHECK (col IN ('a','b',…))` constraint is an enum by another name, and
|
|
3
|
+
* both introspectors read one back as SQL text: SQLite stores the `CREATE
|
|
4
|
+
* TABLE` verbatim, Postgres hands back a normalised `pg_get_constraintdef`.
|
|
5
|
+
* The parsing they need differs, but the quoting rules do not, so both go
|
|
6
|
+
* through here.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Blank out `--` line comments and `/* … *\/` block comments, leaving a string
|
|
10
|
+
* of the same shape so offsets and line breaks still line up.
|
|
11
|
+
*
|
|
12
|
+
* Quoted text is skipped rather than scanned: `'a--b'` is a value, not a
|
|
13
|
+
* comment. The reverse — reading a comment as SQL — is what makes this
|
|
14
|
+
* necessary at all: an apostrophe in `-- the clinician's copy` opens a string
|
|
15
|
+
* that runs to the next quote in the real list, and a bracket in `-- (see
|
|
16
|
+
* below)` closes the list early. Either way the CHECK survives as a union that
|
|
17
|
+
* is wrong, which is worse than no union.
|
|
18
|
+
*/
|
|
19
|
+
export declare function stripSqlComments(sql: string): string;
|
|
20
|
+
/** The single-quoted literals in `text`, with doubled quotes unescaped. */
|
|
21
|
+
export declare function quotedValues(text: string): string[];
|
|
22
|
+
/**
|
|
23
|
+
* Map each column constrained by a `CHECK (col IN (…))` in a `CREATE TABLE`
|
|
24
|
+
* statement to its allowed values. Only the positive `col IN (…)` form is
|
|
25
|
+
* recognised (the convention); `NOT IN`, ranges and boolean expressions are
|
|
26
|
+
* left for the column to stay a plain string.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseCheckEnumsFromDdl(ddl: string): Map<string, string[]>;
|
|
29
|
+
/**
|
|
30
|
+
* The allowed values in a Postgres check constraint, or `undefined` when the
|
|
31
|
+
* constraint is not an enumeration.
|
|
32
|
+
*
|
|
33
|
+
* Postgres does not store the `IN (…)` you wrote — `pg_get_constraintdef`
|
|
34
|
+
* rewrites it as `= ANY (ARRAY[…])`, with each element cast to the column's
|
|
35
|
+
* type. Matching that shape is deliberate: `NOT IN` normalises to `<> ALL
|
|
36
|
+
* (ARRAY[…])` and a range to `>`/`<`, and neither is an enum.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseCheckEnumValues(constraintDef: string): string[] | undefined;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `CHECK (col IN ('a','b',…))` constraint is an enum by another name, and
|
|
3
|
+
* both introspectors read one back as SQL text: SQLite stores the `CREATE
|
|
4
|
+
* TABLE` verbatim, Postgres hands back a normalised `pg_get_constraintdef`.
|
|
5
|
+
* The parsing they need differs, but the quoting rules do not, so both go
|
|
6
|
+
* through here.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Blank out `--` line comments and `/* … *\/` block comments, leaving a string
|
|
10
|
+
* of the same shape so offsets and line breaks still line up.
|
|
11
|
+
*
|
|
12
|
+
* Quoted text is skipped rather than scanned: `'a--b'` is a value, not a
|
|
13
|
+
* comment. The reverse — reading a comment as SQL — is what makes this
|
|
14
|
+
* necessary at all: an apostrophe in `-- the clinician's copy` opens a string
|
|
15
|
+
* that runs to the next quote in the real list, and a bracket in `-- (see
|
|
16
|
+
* below)` closes the list early. Either way the CHECK survives as a union that
|
|
17
|
+
* is wrong, which is worse than no union.
|
|
18
|
+
*/
|
|
19
|
+
export function stripSqlComments(sql) {
|
|
20
|
+
let out = '';
|
|
21
|
+
let i = 0;
|
|
22
|
+
while (i < sql.length) {
|
|
23
|
+
const c = sql[i];
|
|
24
|
+
if (c === "'" || c === '"') {
|
|
25
|
+
const end = closingQuote(sql, i);
|
|
26
|
+
out += sql.slice(i, end);
|
|
27
|
+
i = end;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (c === '-' && sql[i + 1] === '-') {
|
|
31
|
+
while (i < sql.length && sql[i] !== '\n') {
|
|
32
|
+
out += ' ';
|
|
33
|
+
i++;
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (c === '/' && sql[i + 1] === '*') {
|
|
38
|
+
const close = sql.indexOf('*/', i + 2);
|
|
39
|
+
const end = close === -1 ? sql.length : close + 2;
|
|
40
|
+
for (let k = i; k < end; k++)
|
|
41
|
+
out += sql[k] === '\n' ? '\n' : ' ';
|
|
42
|
+
i = end;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
out += c;
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/** Index just past the quoted run starting at `start`, doubled quotes included. */
|
|
51
|
+
function closingQuote(sql, start) {
|
|
52
|
+
const quote = sql[start];
|
|
53
|
+
let i = start + 1;
|
|
54
|
+
while (i < sql.length) {
|
|
55
|
+
if (sql[i] === quote) {
|
|
56
|
+
if (sql[i + 1] === quote) {
|
|
57
|
+
i += 2;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
return i + 1;
|
|
61
|
+
}
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
return sql.length;
|
|
65
|
+
}
|
|
66
|
+
/** The single-quoted literals in `text`, with doubled quotes unescaped. */
|
|
67
|
+
export function quotedValues(text) {
|
|
68
|
+
return [...text.matchAll(/'((?:[^']|'')*)'/g)].map((m) => m[1].replace(/''/g, "'"));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Map each column constrained by a `CHECK (col IN (…))` in a `CREATE TABLE`
|
|
72
|
+
* statement to its allowed values. Only the positive `col IN (…)` form is
|
|
73
|
+
* recognised (the convention); `NOT IN`, ranges and boolean expressions are
|
|
74
|
+
* left for the column to stay a plain string.
|
|
75
|
+
*/
|
|
76
|
+
export function parseCheckEnumsFromDdl(ddl) {
|
|
77
|
+
const out = new Map();
|
|
78
|
+
const sql = stripSqlComments(ddl);
|
|
79
|
+
const checkIn = /CHECK\s*\(\s*"?(\w+)"?\s+IN\s*\(([^)]*)\)/gi;
|
|
80
|
+
let m;
|
|
81
|
+
while ((m = checkIn.exec(sql))) {
|
|
82
|
+
const values = quotedValues(m[2]);
|
|
83
|
+
if (values.length > 0)
|
|
84
|
+
out.set(m[1], values);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The allowed values in a Postgres check constraint, or `undefined` when the
|
|
90
|
+
* constraint is not an enumeration.
|
|
91
|
+
*
|
|
92
|
+
* Postgres does not store the `IN (…)` you wrote — `pg_get_constraintdef`
|
|
93
|
+
* rewrites it as `= ANY (ARRAY[…])`, with each element cast to the column's
|
|
94
|
+
* type. Matching that shape is deliberate: `NOT IN` normalises to `<> ALL
|
|
95
|
+
* (ARRAY[…])` and a range to `>`/`<`, and neither is an enum.
|
|
96
|
+
*/
|
|
97
|
+
export function parseCheckEnumValues(constraintDef) {
|
|
98
|
+
const any = /=\s*ANY\s*\(\s*\(?\s*ARRAY\s*\[([^\]]*)\]/i.exec(constraintDef);
|
|
99
|
+
if (!any)
|
|
100
|
+
return undefined;
|
|
101
|
+
const values = quotedValues(any[1]);
|
|
102
|
+
return values.length > 0 ? values : undefined;
|
|
103
|
+
}
|
|
@@ -641,6 +641,21 @@ export async function generateSchemaTypes(introspector, options) {
|
|
|
641
641
|
const coercionBody = [
|
|
642
642
|
`// Generated by @pikku/cli — do not edit by hand.`,
|
|
643
643
|
`// Run \`pikku db migrate\` to refresh.`,
|
|
644
|
+
`//`,
|
|
645
|
+
`// Nothing applies this for you. Attach it where the kysely singleton is`,
|
|
646
|
+
`// created, in createSingletonServices:`,
|
|
647
|
+
`//`,
|
|
648
|
+
`// import { createCoercionPlugin } from '@pikku/kysely'`,
|
|
649
|
+
`// import { coercionMap } from '#pikku/db/coercion.gen.js'`,
|
|
650
|
+
`//`,
|
|
651
|
+
`// kysely: existingServices.kysely.withPlugin(`,
|
|
652
|
+
`// createCoercionPlugin({ map: coercionMap }),`,
|
|
653
|
+
`// )`,
|
|
654
|
+
`//`,
|
|
655
|
+
`// Unwired, this file is invisible locally and fatal deployed: the dev`,
|
|
656
|
+
`// driver hydrates a TEXT date into a Date by itself, a deployed one`,
|
|
657
|
+
`// returns the raw string, and the generated schema types the column`,
|
|
658
|
+
`// \`Date\` either way — so tsc flags nothing and no local test can fail.`,
|
|
644
659
|
``,
|
|
645
660
|
`export const coercionMap = {`,
|
|
646
661
|
coercionEntries,
|
|
@@ -12,6 +12,20 @@ export declare class PostgresIntrospector implements DbIntrospector {
|
|
|
12
12
|
connect(): Promise<void>;
|
|
13
13
|
listTables(): Promise<string[]>;
|
|
14
14
|
getColumns(table: string): Promise<ColumnInfo[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Columns constrained to a value list by a `CHECK (col IN (…))`, keyed like
|
|
17
|
+
* {@link getAllColumns} then by column name. Postgres has native enums, but
|
|
18
|
+
* a CHECK is the other half of how a schema says "one of these" — and the
|
|
19
|
+
* one an app written against SQLite as well ends up using, since SQLite has
|
|
20
|
+
* no enums at all. Left as plain `string`, every caller narrows it by hand.
|
|
21
|
+
*
|
|
22
|
+
* Only single-column constraints are read: `conkey` naming two columns means
|
|
23
|
+
* neither is restricted to the list on its own.
|
|
24
|
+
*
|
|
25
|
+
* `table` limits the sweep to one table; omitted, it covers the schema. Both
|
|
26
|
+
* are one round-trip — see {@link getAllColumns}.
|
|
27
|
+
*/
|
|
28
|
+
private getCheckEnums;
|
|
15
29
|
getForeignKeys(table: string): Promise<ForeignKeyInfo[]>;
|
|
16
30
|
getAllColumns(): Promise<Map<string, ColumnInfo[]>>;
|
|
17
31
|
getAllForeignKeys(): Promise<Map<string, ForeignKeyInfo[]>>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Pool } from 'pg';
|
|
2
2
|
import { MIGRATION_TRACKING_TABLE } from '../db-migrator.js';
|
|
3
|
+
import { parseCheckEnumValues } from '../check-enums.js';
|
|
3
4
|
/** Table display name matching `listTables` (schema-qualified unless `public`). */
|
|
4
5
|
function tableKey(schema, table) {
|
|
5
6
|
return schema === 'public' ? table : `${schema}.${table}`;
|
|
@@ -85,6 +86,8 @@ export class PostgresIntrospector {
|
|
|
85
86
|
FROM information_schema.columns c
|
|
86
87
|
WHERE c.table_schema = $2 AND c.table_name = $1
|
|
87
88
|
ORDER BY c.ordinal_position`, [tableName, schema]);
|
|
89
|
+
const checkEnums = (await this.getCheckEnums({ schema, name: tableName })).get(table) ??
|
|
90
|
+
new Map();
|
|
88
91
|
return result.rows.map((r) => ({
|
|
89
92
|
name: r.column_name,
|
|
90
93
|
type: resolveColumnType(r.data_type, r.udt_name),
|
|
@@ -93,8 +96,51 @@ export class PostgresIntrospector {
|
|
|
93
96
|
pk: Boolean(r.is_pk),
|
|
94
97
|
defaultValue: r.column_default,
|
|
95
98
|
generated: r.is_generated === 'ALWAYS',
|
|
99
|
+
enumValues: checkEnums.get(r.column_name),
|
|
96
100
|
}));
|
|
97
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Columns constrained to a value list by a `CHECK (col IN (…))`, keyed like
|
|
104
|
+
* {@link getAllColumns} then by column name. Postgres has native enums, but
|
|
105
|
+
* a CHECK is the other half of how a schema says "one of these" — and the
|
|
106
|
+
* one an app written against SQLite as well ends up using, since SQLite has
|
|
107
|
+
* no enums at all. Left as plain `string`, every caller narrows it by hand.
|
|
108
|
+
*
|
|
109
|
+
* Only single-column constraints are read: `conkey` naming two columns means
|
|
110
|
+
* neither is restricted to the list on its own.
|
|
111
|
+
*
|
|
112
|
+
* `table` limits the sweep to one table; omitted, it covers the schema. Both
|
|
113
|
+
* are one round-trip — see {@link getAllColumns}.
|
|
114
|
+
*/
|
|
115
|
+
async getCheckEnums(table) {
|
|
116
|
+
const result = await this.client.query(`SELECT n.nspname AS table_schema,
|
|
117
|
+
cl.relname AS table_name,
|
|
118
|
+
a.attname AS column_name,
|
|
119
|
+
pg_get_constraintdef(co.oid) AS def
|
|
120
|
+
FROM pg_constraint co
|
|
121
|
+
JOIN pg_class cl ON cl.oid = co.conrelid
|
|
122
|
+
JOIN pg_namespace n ON n.oid = cl.relnamespace
|
|
123
|
+
JOIN pg_attribute a ON a.attrelid = co.conrelid AND a.attnum = co.conkey[1]
|
|
124
|
+
WHERE co.contype = 'c'
|
|
125
|
+
AND array_length(co.conkey, 1) = 1
|
|
126
|
+
AND n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
|
|
127
|
+
AND n.nspname NOT LIKE 'pg_temp_%'
|
|
128
|
+
AND ($1::text IS NULL OR (n.nspname = $1 AND cl.relname = $2))`, [table?.schema ?? null, table?.name ?? null]);
|
|
129
|
+
const byTable = new Map();
|
|
130
|
+
for (const r of result.rows) {
|
|
131
|
+
const values = parseCheckEnumValues(r.def);
|
|
132
|
+
if (!values)
|
|
133
|
+
continue;
|
|
134
|
+
const key = tableKey(r.table_schema, r.table_name);
|
|
135
|
+
let cols = byTable.get(key);
|
|
136
|
+
if (!cols) {
|
|
137
|
+
cols = new Map();
|
|
138
|
+
byTable.set(key, cols);
|
|
139
|
+
}
|
|
140
|
+
cols.set(r.column_name, values);
|
|
141
|
+
}
|
|
142
|
+
return byTable;
|
|
143
|
+
}
|
|
98
144
|
async getForeignKeys(table) {
|
|
99
145
|
const dotIdx = table.indexOf('.');
|
|
100
146
|
const schema = dotIdx >= 0 ? table.slice(0, dotIdx) : 'public';
|
|
@@ -153,6 +199,7 @@ export class PostgresIntrospector {
|
|
|
153
199
|
WHERE c.table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
|
|
154
200
|
AND c.table_schema NOT LIKE 'pg_temp_%'
|
|
155
201
|
ORDER BY c.table_schema, c.table_name, c.ordinal_position`);
|
|
202
|
+
const checkEnums = await this.getCheckEnums();
|
|
156
203
|
const byTable = new Map();
|
|
157
204
|
for (const r of result.rows) {
|
|
158
205
|
if (isMigrationTracking(r.table_name))
|
|
@@ -171,6 +218,7 @@ export class PostgresIntrospector {
|
|
|
171
218
|
pk: Boolean(r.is_pk),
|
|
172
219
|
defaultValue: r.column_default,
|
|
173
220
|
generated: r.is_generated === 'ALWAYS',
|
|
221
|
+
enumValues: checkEnums.get(key)?.get(r.column_name),
|
|
174
222
|
});
|
|
175
223
|
}
|
|
176
224
|
return byTable;
|
|
@@ -7,10 +7,9 @@ export declare class SqliteIntrospector implements DbIntrospector {
|
|
|
7
7
|
getColumns(table: string): Promise<ColumnInfo[]>;
|
|
8
8
|
/**
|
|
9
9
|
* SQLite has no native enums, but a `CHECK (col IN ('a','b',…))` constraint is
|
|
10
|
-
* an enum by another name.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `NOT IN`, ranges, and boolean expressions are left as plain `string`.
|
|
10
|
+
* an enum by another name. Read back the table's stored `CREATE TABLE` DDL —
|
|
11
|
+
* which SQLite keeps verbatim, comments included — and map each constrained
|
|
12
|
+
* column to its allowed values so codegen can type it as a union.
|
|
14
13
|
*/
|
|
15
14
|
private parseCheckEnums;
|
|
16
15
|
getForeignKeys(table: string): Promise<ForeignKeyInfo[]>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MIGRATION_TRACKING_TABLE } from '../db-migrator.js';
|
|
2
|
+
import { parseCheckEnumsFromDdl } from '../check-enums.js';
|
|
2
3
|
const SKIP_TABLES = new Set(['sqlite_sequence', MIGRATION_TRACKING_TABLE]);
|
|
3
4
|
export class SqliteIntrospector {
|
|
4
5
|
db;
|
|
@@ -34,28 +35,15 @@ export class SqliteIntrospector {
|
|
|
34
35
|
}
|
|
35
36
|
/**
|
|
36
37
|
* SQLite has no native enums, but a `CHECK (col IN ('a','b',…))` constraint is
|
|
37
|
-
* an enum by another name.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* `NOT IN`, ranges, and boolean expressions are left as plain `string`.
|
|
38
|
+
* an enum by another name. Read back the table's stored `CREATE TABLE` DDL —
|
|
39
|
+
* which SQLite keeps verbatim, comments included — and map each constrained
|
|
40
|
+
* column to its allowed values so codegen can type it as a union.
|
|
41
41
|
*/
|
|
42
42
|
parseCheckEnums(table) {
|
|
43
|
-
const out = new Map();
|
|
44
43
|
const row = this.db
|
|
45
44
|
.prepare(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
46
45
|
.get(table);
|
|
47
|
-
|
|
48
|
-
if (!ddl)
|
|
49
|
-
return out;
|
|
50
|
-
const checkIn = /CHECK\s*\(\s*"?(\w+)"?\s+IN\s*\(([^)]*)\)/gi;
|
|
51
|
-
let m;
|
|
52
|
-
while ((m = checkIn.exec(ddl))) {
|
|
53
|
-
const column = m[1];
|
|
54
|
-
const values = [...m[2].matchAll(/'((?:[^']|'')*)'/g)].map((q) => q[1].replace(/''/g, "'"));
|
|
55
|
-
if (values.length > 0)
|
|
56
|
-
out.set(column, values);
|
|
57
|
-
}
|
|
58
|
-
return out;
|
|
46
|
+
return row?.sql ? parseCheckEnumsFromDdl(row.sql) : new Map();
|
|
59
47
|
}
|
|
60
48
|
async getForeignKeys(table) {
|
|
61
49
|
const escaped = `"${table.replace(/"/g, '""')}"`;
|
|
@@ -12,12 +12,15 @@ export interface VirtualUserGenOutput {
|
|
|
12
12
|
* `pikku persona run` does the same thing from a terminal; this is the same run
|
|
13
13
|
* started over RPC, from CI or from a console, with the result kept.
|
|
14
14
|
*
|
|
15
|
-
* A virtual user is NOT a workflow
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
15
|
+
* A virtual user is still NOT a workflow: it explores, so no two attempts take
|
|
16
|
+
* the same steps and there is nothing to replay. What it does need is a
|
|
17
|
+
* trigger. `runVirtualUser` writes the record and returns the id; the run
|
|
18
|
+
* itself is dispatched onto `pikku-virtual-user-runs`, at one attempt, because
|
|
19
|
+
* a queue is the only dispatch that survives a deployment which puts each
|
|
20
|
+
* function in its own unit — there is no in-process promise to leave running
|
|
21
|
+
* there, and an RPC to a function nothing triggers has nowhere to land. A
|
|
22
|
+
* project with no queue service keeps the in-process dispatch, which is
|
|
23
|
+
* correct for the single process it runs in.
|
|
21
24
|
*
|
|
22
25
|
* Emitted as two files. The schemas are zod, and the inspector reads a zod
|
|
23
26
|
* schema by importing the module that declares it — which it cannot do for the
|
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
* `pikku persona run` does the same thing from a terminal; this is the same run
|
|
9
9
|
* started over RPC, from CI or from a console, with the result kept.
|
|
10
10
|
*
|
|
11
|
-
* A virtual user is NOT a workflow
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* A virtual user is still NOT a workflow: it explores, so no two attempts take
|
|
12
|
+
* the same steps and there is nothing to replay. What it does need is a
|
|
13
|
+
* trigger. `runVirtualUser` writes the record and returns the id; the run
|
|
14
|
+
* itself is dispatched onto `pikku-virtual-user-runs`, at one attempt, because
|
|
15
|
+
* a queue is the only dispatch that survives a deployment which puts each
|
|
16
|
+
* function in its own unit — there is no in-process promise to leave running
|
|
17
|
+
* there, and an RPC to a function nothing triggers has nowhere to land. A
|
|
18
|
+
* project with no queue service keeps the in-process dispatch, which is
|
|
19
|
+
* correct for the single process it runs in.
|
|
17
20
|
*
|
|
18
21
|
* Emitted as two files. The schemas are zod, and the inspector reads a zod
|
|
19
22
|
* schema by importing the module that declares it — which it cannot do for the
|
|
@@ -265,6 +268,7 @@ export const ExecuteVirtualUserRunOutput = z.object({
|
|
|
265
268
|
*/
|
|
266
269
|
import { pikkuFunc, pikkuSessionlessFunc, type Session } from '${leaf('function')}'
|
|
267
270
|
import { pikkuMiddleware } from '${leaf('middleware')}'
|
|
271
|
+
import { wireQueueWorker } from '${leaf('queue')}'
|
|
268
272
|
import { defineScope } from '${leaf('scopes')}'
|
|
269
273
|
import {
|
|
270
274
|
executeVirtualUserRun as executeRun,
|
|
@@ -326,7 +330,11 @@ export const runVirtualUser = pikkuFunc({
|
|
|
326
330
|
scopes: ['virtualUser:run'],
|
|
327
331
|
input: RunVirtualUserInput,
|
|
328
332
|
output: RunVirtualUserOutput,
|
|
329
|
-
func: async (
|
|
333
|
+
func: async (
|
|
334
|
+
{ virtualUserRunStore, config, queueService },
|
|
335
|
+
input,
|
|
336
|
+
{ session, rpc }
|
|
337
|
+
) => {
|
|
330
338
|
const { runId, persona, disposition, goals, memory, seed } =
|
|
331
339
|
await startVirtualUserRun({
|
|
332
340
|
store: virtualUserRunStore,
|
|
@@ -345,26 +353,35 @@ export const runVirtualUser = pikkuFunc({
|
|
|
345
353
|
startedBy: session?.userId ?? null,
|
|
346
354
|
})
|
|
347
355
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
356
|
+
const job = {
|
|
357
|
+
runId,
|
|
358
|
+
persona,
|
|
359
|
+
disposition,
|
|
360
|
+
goals,
|
|
361
|
+
memory,
|
|
362
|
+
seed,
|
|
363
|
+
budget: input.budget,
|
|
364
|
+
// Rides the dispatch and nothing else. The run record is deliberately
|
|
365
|
+
// not given it: a record outlives the run, and a credential in a row
|
|
366
|
+
// somebody can read back is a credential leak.
|
|
367
|
+
operatorToken: input.operatorToken,
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (queueService) {
|
|
371
|
+
// One attempt. A run is an exploration, so a redelivery is a second
|
|
372
|
+
// different outing writing into a record that already has an outcome.
|
|
373
|
+
await queueService.add('pikku-virtual-user-runs', job, {
|
|
374
|
+
attempts: 1,
|
|
375
|
+
pikkuUserId: session?.userId,
|
|
366
376
|
})
|
|
367
|
-
|
|
377
|
+
} else {
|
|
378
|
+
// Deliberately not awaited: a run takes minutes, and a held-open request
|
|
379
|
+
// survives neither a rollout nor a proxy timeout. executeVirtualUserRun
|
|
380
|
+
// writes both outcomes to the record itself, so the only thing left to
|
|
381
|
+
// handle here is a rejection escaping the promise — without the catch it
|
|
382
|
+
// becomes an unhandled rejection and takes the process with it.
|
|
383
|
+
void rpc!.invoke('executeVirtualUserRun', job).catch(() => {})
|
|
384
|
+
}
|
|
368
385
|
|
|
369
386
|
return { runId }
|
|
370
387
|
},
|
|
@@ -560,7 +577,8 @@ export const tickVirtualUserSchedules = pikkuSessionlessFunc<void, void>({
|
|
|
560
577
|
|
|
561
578
|
/**
|
|
562
579
|
* The run itself. Not exposed: it is dispatched by \`runVirtualUser\` and has no
|
|
563
|
-
* caller of its own
|
|
580
|
+
* caller of its own — but it is wired to a queue below, which is what gives it
|
|
581
|
+
* a unit of its own under a per-function deploy.
|
|
564
582
|
*/
|
|
565
583
|
export const executeVirtualUserRun = pikkuSessionlessFunc({
|
|
566
584
|
tags: ['pikku'],
|
|
@@ -581,6 +599,12 @@ export const executeVirtualUserRun = pikkuSessionlessFunc({
|
|
|
581
599
|
...input,
|
|
582
600
|
}),
|
|
583
601
|
})
|
|
602
|
+
|
|
603
|
+
wireQueueWorker({
|
|
604
|
+
name: 'pikku-virtual-user-runs',
|
|
605
|
+
tags: ['pikku'],
|
|
606
|
+
func: executeVirtualUserRun,
|
|
607
|
+
})
|
|
584
608
|
`;
|
|
585
609
|
return { schemas, functions };
|
|
586
610
|
};
|