@pikku/inspector 0.12.44 → 0.12.46
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 +126 -0
- package/dist/utils/detect-schema-vendor.d.ts +1 -1
- package/dist/utils/detect-schema-vendor.js +26 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +19 -1
- package/package.json +3 -3
- package/src/add/add-auth.test.ts +4 -4
- package/src/utils/detect-schema-vendor.test.ts +123 -0
- package/src/utils/detect-schema-vendor.ts +31 -1
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +21 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,129 @@
|
|
|
1
|
+
## 0.12.46
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- a8f9a7d: Distinguish an unresolvable schema type from an unsupported schema library.
|
|
6
|
+
|
|
7
|
+
Both failures shared one message, so a plain `z.object({...})` whose type TypeScript
|
|
8
|
+
could not resolve — a file outside tsconfig `include`, or a generated file such as
|
|
9
|
+
`.pikku/db/zod.gen.ts` that had not been written yet — was reported as
|
|
10
|
+
"Ensure your schema is imported from a supported validation library". The schema is
|
|
11
|
+
dropped either way (`inputSchemaName: null`, no generated schema file), so the message
|
|
12
|
+
is the only signal that a function has silently lost its input contract, and that
|
|
13
|
+
advice is unactionable when the schema already is zod.
|
|
14
|
+
|
|
15
|
+
- eaabcbf: fix(inspector): don't reject plain destructuring in DSL workflows
|
|
16
|
+
|
|
17
|
+
`extractDestructuredDeclaration` reported "Destructuring a step result is not
|
|
18
|
+
supported in DSL workflows" for EVERY destructuring statement whose initializer
|
|
19
|
+
wasn't `await Promise.all([...])` — including ordinary local bindings that
|
|
20
|
+
involve no step at all, such as `const { runId } = input`.
|
|
21
|
+
|
|
22
|
+
Destructuring the workflow's own input is the single most idiomatic line in a
|
|
23
|
+
DSL workflow, so this rejected working workflows wholesale under a message
|
|
24
|
+
about step results that named nothing the author had written.
|
|
25
|
+
|
|
26
|
+
The diagnostic now fires only when the destructured initializer really is a
|
|
27
|
+
step (`await workflow.do(...)`, a parallel group, or a parallel fanout).
|
|
28
|
+
Anything else passes through as a non-step, exactly as the identifier path
|
|
29
|
+
already does for `const x = someLocal`.
|
|
30
|
+
|
|
31
|
+
- Updated dependencies [f11675f]
|
|
32
|
+
- @pikku/core@0.12.68
|
|
33
|
+
|
|
34
|
+
## 0.12.45
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- ae4f59a: Gate admin capabilities on scopes, and scaffold user management
|
|
39
|
+
|
|
40
|
+
Admin capabilities were gated on `user.role === 'admin'` — a single text column
|
|
41
|
+
meaning "can do everything". Impersonating a user, rebinding a shared
|
|
42
|
+
credential and reading the user directory are distinct capabilities that one
|
|
43
|
+
user can hold independently, so they are now scopes on an `admin` tree:
|
|
44
|
+
|
|
45
|
+
| Gate | Scope |
|
|
46
|
+
| -------------------------------------- | ------------------------ |
|
|
47
|
+
| impersonation | `admin:impersonate` |
|
|
48
|
+
| `credentialOAuth`'s `canLinkSingleton` | `admin:credentials:link` |
|
|
49
|
+
| reading the user directory | `admin:users:list` |
|
|
50
|
+
| creating a user out of band | `admin:users:create` |
|
|
51
|
+
| ban / unban | `admin:users:ban` |
|
|
52
|
+
| delete a user | `admin:users:remove` |
|
|
53
|
+
| revoke a user's sessions | `admin:users:sessions` |
|
|
54
|
+
| set a user's password | `admin:users:password` |
|
|
55
|
+
|
|
56
|
+
Holding the bare `admin` scope satisfies all of them via pikku's existing
|
|
57
|
+
parent-grant rule, so it is a one-for-one replacement for the old role.
|
|
58
|
+
|
|
59
|
+
better-auth's `admin()` plugin is still what implements ban, delete,
|
|
60
|
+
session-revocation and set-password, so it stays. Its `user.role` column is no
|
|
61
|
+
longer something pikku grants: it is _projected_ from the scope store when a
|
|
62
|
+
session is built, and only from the scopes whose capability better-auth's own
|
|
63
|
+
endpoints gate on the caller's role. Someone granted `admin:users:list` can read
|
|
64
|
+
the directory — which goes straight to the auth adapter — without gaining the
|
|
65
|
+
power to ban, and revoking a scope demotes the role on the next sign-in. Scopes
|
|
66
|
+
remain the single source of truth.
|
|
67
|
+
|
|
68
|
+
New `scaffold.userAdmin` in `pikku.config.json` generates the whole set —
|
|
69
|
+
`pikkuAdminListUsers`, `pikkuAdminCreateUser`, `pikkuAdminSetUserBanned`,
|
|
70
|
+
`pikkuAdminRemoveUser`, `pikkuAdminRevokeUserSessions` and
|
|
71
|
+
`pikkuAdminSetUserPassword` — into your project. Listing or banning a user is
|
|
72
|
+
ordinary application behaviour and must not require installing the console.
|
|
73
|
+
Codegen fails with an actionable error if better-auth is wired without
|
|
74
|
+
`admin()`. The console's Users page calls these same functions, showing each
|
|
75
|
+
action only where the caller holds its scope.
|
|
76
|
+
|
|
77
|
+
Every scaffold now emits a directory named for its domain — `scaffold/admin/`,
|
|
78
|
+
`scaffold/rpc/`, `scaffold/agent/`, `scaffold/auth/`, `scaffold/console/`,
|
|
79
|
+
`scaffold/graph/`, `scaffold/realtime/`, `scaffold/scenarios/`,
|
|
80
|
+
`scaffold/webhook/`, `scaffold/workflow/` — holding its wiring file beside a
|
|
81
|
+
`*.schemas.gen.ts` sibling, and every generated payload is a zod schema instead
|
|
82
|
+
of a TypeScript generic. The schemas have to stand alone: the inspector reads a
|
|
83
|
+
zod schema by importing the module that declares it, which it cannot do for a
|
|
84
|
+
wiring file whose relative pikku-types import per-unit deploy codegen rewrites.
|
|
85
|
+
|
|
86
|
+
Resolving a schema by reference rather than by name also fixes the agent HTTP
|
|
87
|
+
surface. `agentCaller` and `agentStreamCaller` take the same payload but had to
|
|
88
|
+
repeat the type literal verbatim in each generic position, because the extractor
|
|
89
|
+
synthesised the schema name from the _function_ name and so recorded an
|
|
90
|
+
`inputSchemaName` with no schema behind it whenever the two shared a named
|
|
91
|
+
alias — every agent call through that alias failed with `MissingSchemaError`.
|
|
92
|
+
One `AgentCall` schema now backs both.
|
|
93
|
+
|
|
94
|
+
Where a payload's shape belongs to `@pikku/core` (`WorkflowRunStatus`,
|
|
95
|
+
`FunctionCoverageReport`, `StubCall[]`) the generated function carries no
|
|
96
|
+
`output` schema and the inspector infers it from the handler's return type;
|
|
97
|
+
re-declaring a core type in zod would be a second definition free to drift.
|
|
98
|
+
|
|
99
|
+
Upgrading rewrites the layout in place: codegen prunes the pre-directory copy of
|
|
100
|
+
each scaffold file before it inspects the source tree, since the old flat file
|
|
101
|
+
still wires the same routes and leaving it behind would wire everything twice.
|
|
102
|
+
|
|
103
|
+
`@pikku/core` gains `hasScopes(required, held)`, the non-throwing counterpart to
|
|
104
|
+
`verifyScopes`, and declares `auth` on `CoreSingletonServices` — the auth
|
|
105
|
+
instance the generated `pikkuServices` wrapper already injected but never typed.
|
|
106
|
+
A scope root declared twice (an addon and its host both contributing the same
|
|
107
|
+
`admin` tree) now flattens to one entry per id instead of emitting it twice.
|
|
108
|
+
|
|
109
|
+
BREAKING: there is no role fallback for the scope-gated capabilities. An app
|
|
110
|
+
that relied on the old default must register a `ScopeService` and grant `admin`
|
|
111
|
+
(or a narrower `admin:*` scope). Every gate fails closed and warns when no
|
|
112
|
+
`ScopeService` is registered. `delegatedAuth`'s `defaultRole`/`mapRole` now
|
|
113
|
+
grant a pikku role through the `ScopeService` instead of writing better-auth's
|
|
114
|
+
`role` column, and the `credentialOAuth` platform user no longer sets `banned`.
|
|
115
|
+
|
|
116
|
+
BREAKING: the console reads its user directory over the scaffolded
|
|
117
|
+
`pikkuAdminListUsers` RPC (gated on `admin:users:list`, backed by better-auth's
|
|
118
|
+
`$context.adapter`) instead of `client.admin.listUsers`, and
|
|
119
|
+
`UsersTableUser`/`UsersTableLabels` no longer carry `role` — there is no role
|
|
120
|
+
column to render. `@pikku/addon-console` no longer ships a `console:listUsers`
|
|
121
|
+
function: user management is not the console's job, so a host that wants the
|
|
122
|
+
Users page must enable `scaffold.userAdmin`.
|
|
123
|
+
|
|
124
|
+
- Updated dependencies [ae4f59a]
|
|
125
|
+
- @pikku/core@0.12.67
|
|
126
|
+
|
|
1
127
|
## 0.12.44
|
|
2
128
|
|
|
3
129
|
### Patch Changes
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
1
2
|
import { ErrorCode } from '../error-codes.js';
|
|
3
|
+
/**
|
|
4
|
+
* Whether the checker could type the identifier at all.
|
|
5
|
+
*
|
|
6
|
+
* `any` here is not a schema written in an exotic library — it is TypeScript telling us
|
|
7
|
+
* it could not resolve the declaration, which happens when the schema comes from a file
|
|
8
|
+
* outside the program (not matched by tsconfig `include`) or from a generated file that
|
|
9
|
+
* has not been written yet (`.pikku/db/zod.gen.ts` before `pikku db generate` runs).
|
|
10
|
+
* The distinction matters because the two have opposite fixes, and conflating them told
|
|
11
|
+
* users to "import from a supported validation library" about a plain `z.object`.
|
|
12
|
+
*/
|
|
13
|
+
const isUnresolved = (type) => (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
|
|
2
14
|
/**
|
|
3
15
|
* Detect the schema vendor by tracing the type back to its library origin.
|
|
4
16
|
* This handles locally-defined schemas like `export const MySchema = z.object({...})`
|
|
@@ -67,6 +79,20 @@ export const detectSchemaVendor = (identifier, checker) => {
|
|
|
67
79
|
export const detectSchemaVendorOrError = (identifier, checker, logger, context, sourceFile) => {
|
|
68
80
|
const vendor = detectSchemaVendor(identifier, checker);
|
|
69
81
|
if (vendor === 'unknown') {
|
|
82
|
+
// Two different failures used to share one message. Reporting the wrong one is not a
|
|
83
|
+
// cosmetic problem: the schema is dropped either way (`inputSchemaName: null`, no
|
|
84
|
+
// generated schema file), so the message is the only thing standing between the user
|
|
85
|
+
// and a function that silently validates nothing — and "use a supported validation
|
|
86
|
+
// library" is unactionable when the schema already IS `z.object({...})`.
|
|
87
|
+
if (isUnresolved(checker.getTypeAtLocation(identifier))) {
|
|
88
|
+
logger.critical(ErrorCode.INLINE_SCHEMA, `${context} schema '${identifier.text}' could not be TYPED from '${sourceFile}' — ` +
|
|
89
|
+
`TypeScript resolved it to 'any', so its vendor cannot be traced. This is a ` +
|
|
90
|
+
`resolution problem, not a schema problem: the file it is imported from is either ` +
|
|
91
|
+
`outside your tsconfig 'include', or has not been generated yet (a schema built ` +
|
|
92
|
+
`from '.pikku/db/zod.gen.ts' needs 'pikku db generate' to have run first). ` +
|
|
93
|
+
`Fix the import so the type resolves, then re-run.`);
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
70
96
|
logger.critical(ErrorCode.INLINE_SCHEMA, `${context} schema vendor could not be determined from '${sourceFile}'. ` +
|
|
71
97
|
`Supported vendors: zod, valibot, arktype, @effect/schema. ` +
|
|
72
98
|
`Ensure your schema is imported from a supported validation library.`);
|
|
@@ -304,8 +304,15 @@ function extractVariableDeclaration(statement, context) {
|
|
|
304
304
|
*
|
|
305
305
|
* `const [a, b] = await Promise.all([...])` is the idiomatic way to run steps
|
|
306
306
|
* in parallel and keep both results, so each element of the pattern is bound to
|
|
307
|
-
* the matching child step's output. Every other destructuring
|
|
307
|
+
* the matching child step's output. Every other destructuring OF A STEP reports a
|
|
308
308
|
* diagnostic rather than silently dropping the step.
|
|
309
|
+
*
|
|
310
|
+
* A destructure whose initializer is NOT a step is just an ordinary local binding
|
|
311
|
+
* — `const { runId } = input`, `const { a } = someObject` — and has nothing to do
|
|
312
|
+
* with step results. Those pass through as non-steps, exactly as the identifier
|
|
313
|
+
* path already does for `const x = someLocal`. Reporting them was rejecting the
|
|
314
|
+
* single most idiomatic line in a DSL workflow (destructuring the workflow's own
|
|
315
|
+
* input), under a message about step results that named nothing the author wrote.
|
|
309
316
|
*/
|
|
310
317
|
function extractDestructuredDeclaration(statement, decl, context) {
|
|
311
318
|
const init = decl.initializer;
|
|
@@ -342,6 +349,17 @@ function extractDestructuredDeclaration(statement, decl, context) {
|
|
|
342
349
|
}
|
|
343
350
|
}
|
|
344
351
|
}
|
|
352
|
+
// Only a destructure OF A STEP is an error. Anything else is a plain local
|
|
353
|
+
// binding the DSL simply doesn't model as a step.
|
|
354
|
+
const stepCall = init && ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)
|
|
355
|
+
? init.expression
|
|
356
|
+
: null;
|
|
357
|
+
const destructuresAStep = stepCall !== null &&
|
|
358
|
+
(isWorkflowDoCall(stepCall, context.checker) ||
|
|
359
|
+
isParallelGroup(stepCall) ||
|
|
360
|
+
isParallelFanout(stepCall));
|
|
361
|
+
if (!destructuresAStep)
|
|
362
|
+
return null;
|
|
345
363
|
context.errors.push({
|
|
346
364
|
message: `Destructuring a step result is not supported in DSL workflows. Assign it to a variable first (e.g. \`const result = await workflow.do(...)\`) and read its properties.`,
|
|
347
365
|
node: statement,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.46",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"type": "module",
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"zod-to-ts": "^2.1.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@pikku/core": "^0.12.
|
|
47
|
+
"@pikku/core": "^0.12.68"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@pikku/core": "^0.12.
|
|
50
|
+
"@pikku/core": "^0.12.68",
|
|
51
51
|
"@types/node": "^24.11.0"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
package/src/add/add-auth.test.ts
CHANGED
|
@@ -190,11 +190,11 @@ describe('addAuth inspector', () => {
|
|
|
190
190
|
[
|
|
191
191
|
"import { pikkuBetterAuth } from '@pikku/better-auth'",
|
|
192
192
|
"import { betterAuth } from 'better-auth'",
|
|
193
|
-
"import { bearer,
|
|
193
|
+
"import { bearer, apiKey, twoFactor } from 'better-auth/plugins'",
|
|
194
194
|
'export const auth = pikkuBetterAuth(() =>',
|
|
195
195
|
' betterAuth({',
|
|
196
196
|
" socialProviders: { github: { clientId: 'x', clientSecret: 'y' } },",
|
|
197
|
-
' plugins: [bearer(),
|
|
197
|
+
' plugins: [bearer(), apiKey(), twoFactor({ issuer: "pikku" })],',
|
|
198
198
|
' })',
|
|
199
199
|
')',
|
|
200
200
|
].join('\n')
|
|
@@ -204,10 +204,10 @@ describe('addAuth inspector', () => {
|
|
|
204
204
|
try {
|
|
205
205
|
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
206
206
|
assert.equal(criticals.length, 0)
|
|
207
|
-
assert.deepEqual(state.auth.plugins, ['bearer', '
|
|
207
|
+
assert.deepEqual(state.auth.plugins, ['bearer', 'apiKey', 'twoFactor'])
|
|
208
208
|
assert.deepEqual(state.auth.definition?.plugins, [
|
|
209
209
|
'bearer',
|
|
210
|
-
'
|
|
210
|
+
'apiKey',
|
|
211
211
|
'twoFactor',
|
|
212
212
|
])
|
|
213
213
|
} finally {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { test, describe, before, after } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
|
|
4
|
+
import { tmpdir } from 'os'
|
|
5
|
+
import { join } from 'path'
|
|
6
|
+
import * as ts from 'typescript'
|
|
7
|
+
|
|
8
|
+
import { detectSchemaVendorOrError } from './detect-schema-vendor.js'
|
|
9
|
+
|
|
10
|
+
let projectDir: string
|
|
11
|
+
let program: ts.Program
|
|
12
|
+
let checker: ts.TypeChecker
|
|
13
|
+
let sourceFile: ts.SourceFile
|
|
14
|
+
|
|
15
|
+
const criticals: string[] = []
|
|
16
|
+
const logger = {
|
|
17
|
+
debug() {},
|
|
18
|
+
info() {},
|
|
19
|
+
warn() {},
|
|
20
|
+
error() {},
|
|
21
|
+
critical(_code: unknown, message: string) {
|
|
22
|
+
criticals.push(message)
|
|
23
|
+
},
|
|
24
|
+
} as any
|
|
25
|
+
|
|
26
|
+
const initializerIdentifier = (name: string): ts.Identifier => {
|
|
27
|
+
let found: ts.Identifier | undefined
|
|
28
|
+
const visit = (node: ts.Node) => {
|
|
29
|
+
if (
|
|
30
|
+
ts.isVariableDeclaration(node) &&
|
|
31
|
+
ts.isIdentifier(node.name) &&
|
|
32
|
+
node.name.text === name &&
|
|
33
|
+
node.initializer &&
|
|
34
|
+
ts.isIdentifier(node.initializer)
|
|
35
|
+
) {
|
|
36
|
+
found = node.initializer
|
|
37
|
+
}
|
|
38
|
+
ts.forEachChild(node, visit)
|
|
39
|
+
}
|
|
40
|
+
visit(sourceFile)
|
|
41
|
+
assert.ok(found, `no identifier initializer found for '${name}'`)
|
|
42
|
+
return found
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
before(() => {
|
|
46
|
+
projectDir = mkdtempSync(join(tmpdir(), 'pikku-schema-vendor-'))
|
|
47
|
+
mkdirSync(join(projectDir, 'src'))
|
|
48
|
+
writeFileSync(
|
|
49
|
+
join(projectDir, 'src', 'schemas.ts'),
|
|
50
|
+
[
|
|
51
|
+
// Imported from a file that does not exist — exactly what a schema built
|
|
52
|
+
// from an ungenerated '.pikku/db/zod.gen.ts' looks like to the checker.
|
|
53
|
+
`import { UngeneratedSchema } from './not-written-yet.js'`,
|
|
54
|
+
`export const unresolved = UngeneratedSchema`,
|
|
55
|
+
`const plainObject = { a: 1 }`,
|
|
56
|
+
`export const resolvedButNotASchema = plainObject`,
|
|
57
|
+
'',
|
|
58
|
+
].join('\n')
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
program = ts.createProgram([join(projectDir, 'src', 'schemas.ts')], {
|
|
62
|
+
target: ts.ScriptTarget.ESNext,
|
|
63
|
+
module: ts.ModuleKind.NodeNext,
|
|
64
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
65
|
+
strict: true,
|
|
66
|
+
skipLibCheck: true,
|
|
67
|
+
noEmit: true,
|
|
68
|
+
})
|
|
69
|
+
checker = program.getTypeChecker()
|
|
70
|
+
sourceFile = program.getSourceFile(join(projectDir, 'src', 'schemas.ts'))!
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
after(() => {
|
|
74
|
+
rmSync(projectDir, { recursive: true, force: true })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('detectSchemaVendorOrError', () => {
|
|
78
|
+
test('reports a type the checker could not resolve as a resolution failure', () => {
|
|
79
|
+
criticals.length = 0
|
|
80
|
+
|
|
81
|
+
const vendor = detectSchemaVendorOrError(
|
|
82
|
+
initializerIdentifier('unresolved'),
|
|
83
|
+
checker,
|
|
84
|
+
logger,
|
|
85
|
+
`Function 'createTodo' input`,
|
|
86
|
+
'src/schemas.ts'
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
assert.equal(vendor, undefined)
|
|
90
|
+
assert.equal(criticals.length, 1)
|
|
91
|
+
const [message] = criticals
|
|
92
|
+
assert.match(message, /could not be TYPED/)
|
|
93
|
+
assert.match(message, /UngeneratedSchema/)
|
|
94
|
+
assert.match(message, /pikku db generate/)
|
|
95
|
+
// The unactionable advice must NOT be given for a type that simply failed
|
|
96
|
+
// to resolve — the schema may well already be zod.
|
|
97
|
+
assert.doesNotMatch(
|
|
98
|
+
message,
|
|
99
|
+
/Ensure your schema is imported from a supported validation library/
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('still reports a resolvable non-vendor type as an unsupported library', () => {
|
|
104
|
+
criticals.length = 0
|
|
105
|
+
|
|
106
|
+
const vendor = detectSchemaVendorOrError(
|
|
107
|
+
initializerIdentifier('resolvedButNotASchema'),
|
|
108
|
+
checker,
|
|
109
|
+
logger,
|
|
110
|
+
`Function 'createTodo' input`,
|
|
111
|
+
'src/schemas.ts'
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
assert.equal(vendor, undefined)
|
|
115
|
+
assert.equal(criticals.length, 1)
|
|
116
|
+
const [message] = criticals
|
|
117
|
+
assert.match(
|
|
118
|
+
message,
|
|
119
|
+
/Ensure your schema is imported from a supported validation library/
|
|
120
|
+
)
|
|
121
|
+
assert.doesNotMatch(message, /could not be TYPED/)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
2
|
import type { SchemaVendor, InspectorLogger } from '../types.js'
|
|
3
3
|
import { ErrorCode } from '../error-codes.js'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Whether the checker could type the identifier at all.
|
|
7
|
+
*
|
|
8
|
+
* `any` here is not a schema written in an exotic library — it is TypeScript telling us
|
|
9
|
+
* it could not resolve the declaration, which happens when the schema comes from a file
|
|
10
|
+
* outside the program (not matched by tsconfig `include`) or from a generated file that
|
|
11
|
+
* has not been written yet (`.pikku/db/zod.gen.ts` before `pikku db generate` runs).
|
|
12
|
+
* The distinction matters because the two have opposite fixes, and conflating them told
|
|
13
|
+
* users to "import from a supported validation library" about a plain `z.object`.
|
|
14
|
+
*/
|
|
15
|
+
const isUnresolved = (type: ts.Type): boolean =>
|
|
16
|
+
(type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0
|
|
17
|
+
|
|
5
18
|
/**
|
|
6
19
|
* Detect the schema vendor by tracing the type back to its library origin.
|
|
7
20
|
* This handles locally-defined schemas like `export const MySchema = z.object({...})`
|
|
@@ -78,6 +91,23 @@ export const detectSchemaVendorOrError = (
|
|
|
78
91
|
): Exclude<SchemaVendor, 'unknown'> | undefined => {
|
|
79
92
|
const vendor = detectSchemaVendor(identifier, checker)
|
|
80
93
|
if (vendor === 'unknown') {
|
|
94
|
+
// Two different failures used to share one message. Reporting the wrong one is not a
|
|
95
|
+
// cosmetic problem: the schema is dropped either way (`inputSchemaName: null`, no
|
|
96
|
+
// generated schema file), so the message is the only thing standing between the user
|
|
97
|
+
// and a function that silently validates nothing — and "use a supported validation
|
|
98
|
+
// library" is unactionable when the schema already IS `z.object({...})`.
|
|
99
|
+
if (isUnresolved(checker.getTypeAtLocation(identifier))) {
|
|
100
|
+
logger.critical(
|
|
101
|
+
ErrorCode.INLINE_SCHEMA,
|
|
102
|
+
`${context} schema '${identifier.text}' could not be TYPED from '${sourceFile}' — ` +
|
|
103
|
+
`TypeScript resolved it to 'any', so its vendor cannot be traced. This is a ` +
|
|
104
|
+
`resolution problem, not a schema problem: the file it is imported from is either ` +
|
|
105
|
+
`outside your tsconfig 'include', or has not been generated yet (a schema built ` +
|
|
106
|
+
`from '.pikku/db/zod.gen.ts' needs 'pikku db generate' to have run first). ` +
|
|
107
|
+
`Fix the import so the type resolves, then re-run.`
|
|
108
|
+
)
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
81
111
|
logger.critical(
|
|
82
112
|
ErrorCode.INLINE_SCHEMA,
|
|
83
113
|
`${context} schema vendor could not be determined from '${sourceFile}'. ` +
|
|
@@ -447,8 +447,15 @@ function extractVariableDeclaration(
|
|
|
447
447
|
*
|
|
448
448
|
* `const [a, b] = await Promise.all([...])` is the idiomatic way to run steps
|
|
449
449
|
* in parallel and keep both results, so each element of the pattern is bound to
|
|
450
|
-
* the matching child step's output. Every other destructuring
|
|
450
|
+
* the matching child step's output. Every other destructuring OF A STEP reports a
|
|
451
451
|
* diagnostic rather than silently dropping the step.
|
|
452
|
+
*
|
|
453
|
+
* A destructure whose initializer is NOT a step is just an ordinary local binding
|
|
454
|
+
* — `const { runId } = input`, `const { a } = someObject` — and has nothing to do
|
|
455
|
+
* with step results. Those pass through as non-steps, exactly as the identifier
|
|
456
|
+
* path already does for `const x = someLocal`. Reporting them was rejecting the
|
|
457
|
+
* single most idiomatic line in a DSL workflow (destructuring the workflow's own
|
|
458
|
+
* input), under a message about step results that named nothing the author wrote.
|
|
452
459
|
*/
|
|
453
460
|
function extractDestructuredDeclaration(
|
|
454
461
|
statement: ts.VariableStatement,
|
|
@@ -499,6 +506,19 @@ function extractDestructuredDeclaration(
|
|
|
499
506
|
}
|
|
500
507
|
}
|
|
501
508
|
|
|
509
|
+
// Only a destructure OF A STEP is an error. Anything else is a plain local
|
|
510
|
+
// binding the DSL simply doesn't model as a step.
|
|
511
|
+
const stepCall =
|
|
512
|
+
init && ts.isAwaitExpression(init) && ts.isCallExpression(init.expression)
|
|
513
|
+
? init.expression
|
|
514
|
+
: null
|
|
515
|
+
const destructuresAStep =
|
|
516
|
+
stepCall !== null &&
|
|
517
|
+
(isWorkflowDoCall(stepCall, context.checker) ||
|
|
518
|
+
isParallelGroup(stepCall) ||
|
|
519
|
+
isParallelFanout(stepCall))
|
|
520
|
+
if (!destructuresAStep) return null
|
|
521
|
+
|
|
502
522
|
context.errors.push({
|
|
503
523
|
message: `Destructuring a step result is not supported in DSL workflows. Assign it to a variable first (e.g. \`const result = await workflow.do(...)\`) and read its properties.`,
|
|
504
524
|
node: statement,
|