@pikku/inspector 0.12.17 → 0.12.19

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 CHANGED
@@ -1,3 +1,57 @@
1
+ ## 0.12.19
2
+
3
+ ### Patch Changes
4
+
5
+ - fe70fe0: fix(db): make classified columns usable in Kysely queries and emit real zod
6
+
7
+ Two fixes so data-classified DB columns (`@private`/`@pii`/`@secret`, default
8
+ `private`) are usable end-to-end instead of poisoning ordinary app code:
9
+ 1. **Brand marker is now optional** (`{ readonly __classification__?: ... }`)
10
+ in both `@pikku/core` and the `pikku db migrate` schema header. A required
11
+ marker made a plain value (e.g. `string`) unassignable to a branded column
12
+ (`Private<string>`), breaking every Kysely `where`/insert/`.set()` operand —
13
+ any project with classified columns failed to type-check. Optional keeps the
14
+ brand structurally present (so the inspector's PKU910 output check still
15
+ detects it) while letting plain values flow IN. The inspector's level read is
16
+ now union-aware (`'pii' | undefined`) so pii/secret no longer silently
17
+ downgrade to private.
18
+ 2. **Zod codegen resolves classified `ColumnType<>`** to proper scalars instead
19
+ of `z.unknown()`. `pikku db migrate` emits `<Table>Z`/`InsertZ`/`PatchZ` from
20
+ the Select slot, unwrapping the brand and honoring insert-optionality from the
21
+ Insert slot's `| undefined`. Public `Generated<T>`/bare/nested shapes are
22
+ unchanged.
23
+
24
+ - Updated dependencies [fe70fe0]
25
+ - @pikku/core@0.12.31
26
+
27
+ ## 0.12.18
28
+
29
+ ### Patch Changes
30
+
31
+ - 20750fd: feat(workflow): decide step dispatch purely per-function
32
+
33
+ Workflow step execution (inline vs queue dispatch) is now decided entirely by
34
+ the step's function `inline` flag — the workflow-level / run-level `inline`
35
+ meta no longer participates in per-step dispatch.
36
+ - Steps default to **inline**, so a normally-started (queue-backed) workflow
37
+ runs its whole chain in one orchestrator pass instead of one queue
38
+ round-trip per step.
39
+ - A function marked `inline: false` is dispatched via the queue (its own
40
+ worker, retry isolation). When `inline: false` but no `queueService` is
41
+ configured, the step falls back to inline and emits a `logger.warn` instead
42
+ of silently swallowing the misconfiguration.
43
+ - Removed the now-unused workflow-level `inline` from `WorkflowsMeta` /
44
+ `WorkflowRuntimeMeta`, the inspector's workflow extraction, the DSL→graph
45
+ converter, and the deploy analyzer / service inference (which now key off
46
+ the per-function flag). Run-level `inline` is retained: it still controls
47
+ whether a whole run executes in-process without queue infrastructure.
48
+
49
+ - Updated dependencies [cd101a5]
50
+ - Updated dependencies [ac16265]
51
+ - Updated dependencies [a05e864]
52
+ - Updated dependencies [20750fd]
53
+ - @pikku/core@0.12.30
54
+
1
55
  ## 0.12.17
2
56
 
3
57
  ### Patch Changes
@@ -488,6 +488,12 @@ export const addFunctions = (logger, node, checker, state, options) => {
488
488
  const genericTypes = (typeArguments ?? [])
489
489
  .map((tn) => checker.getTypeFromTypeNode(tn))
490
490
  .map((t) => unwrapPromise(checker, t));
491
+ // pikkuChannelConnectionFunc<Out> declares a single generic that is the
492
+ // OUTPUT type — its input is always void (PikkuFunctionSessionless<void, Out>).
493
+ // Every other wrapper reads generic[0] as INPUT, so without this guard the
494
+ // connect handler's output generic is mis-recorded as inputSchemaName and the
495
+ // empty WS handshake fails input validation at connect (1008/403).
496
+ const isChannelConnectionFunc = /ChannelConnection/i.test(expression.text);
491
497
  const capitalizedName = funcIdToTypeName(name);
492
498
  // --- Input Extraction ---
493
499
  let inputNames = [];
@@ -513,7 +519,10 @@ export const addFunctions = (logger, node, checker, state, options) => {
513
519
  inputTypes = [filterType];
514
520
  }
515
521
  }
516
- else if (!isListFunc && genericTypes.length >= 1 && genericTypes[0]) {
522
+ else if (!isChannelConnectionFunc &&
523
+ !isListFunc &&
524
+ genericTypes.length >= 1 &&
525
+ genericTypes[0]) {
517
526
  // Fall back to extracting from generic type arguments
518
527
  const result = getNamesAndTypes(checker, state.functions.typesMap, 'Input', name, genericTypes[0]);
519
528
  inputNames = result.names;
@@ -2,6 +2,7 @@ import * as ts from 'typescript';
2
2
  import { getPropertyValue, assertStringLiteralProperty, } from '../utils/get-property-value.js';
3
3
  import { ErrorCode } from '../error-codes.js';
4
4
  import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js';
5
+ import { parseDurationString } from '@pikku/core';
5
6
  export const createAddKeyedWiring = (config) => {
6
7
  return (logger, node, checker, state, _options) => {
7
8
  if (!ts.isCallExpression(node)) {
@@ -24,6 +25,7 @@ export const createAddKeyedWiring = (config) => {
24
25
  const nameValue = getPropertyValue(obj, 'name');
25
26
  const displayNameValue = getPropertyValue(obj, 'displayName');
26
27
  const descriptionValue = getPropertyValue(obj, 'description');
28
+ const rotationPeriodValue = getPropertyValue(obj, 'rotationPeriod');
27
29
  const idValue = getPropertyValue(obj, config.idField);
28
30
  let schemaVariableName = null;
29
31
  let schemaSourceFile = null;
@@ -74,6 +76,15 @@ export const createAddKeyedWiring = (config) => {
74
76
  logger.critical(ErrorCode.MISSING_NAME, `${config.label} '${nameValue}' is missing the required 'schema' property or schema is not a variable reference.`);
75
77
  return;
76
78
  }
79
+ if (rotationPeriodValue) {
80
+ try {
81
+ parseDurationString(rotationPeriodValue);
82
+ }
83
+ catch {
84
+ logger.critical(ErrorCode.INVALID_VALUE, `${config.label} '${nameValue}' has an invalid 'rotationPeriod': '${rotationPeriodValue}'. Use a duration like '1d', '30day', or '1w'.`);
85
+ return;
86
+ }
87
+ }
77
88
  const sourceFile = node.getSourceFile().fileName;
78
89
  const wiringState = config.getState(state);
79
90
  wiringState.files.add(sourceFile);
@@ -90,6 +101,7 @@ export const createAddKeyedWiring = (config) => {
90
101
  name: nameValue,
91
102
  displayName: displayNameValue,
92
103
  description: descriptionValue || undefined,
104
+ rotationPeriod: rotationPeriodValue || undefined,
93
105
  [config.idField]: idValue,
94
106
  schema: schemaLookupName,
95
107
  sourceFile,
@@ -182,7 +182,6 @@ export const addWorkflow = (logger, node, checker, state) => {
182
182
  let summary;
183
183
  let description;
184
184
  let errors;
185
- let inline;
186
185
  let expose;
187
186
  if (ts.isObjectLiteralExpression(firstArg)) {
188
187
  const metadata = getCommonWireMetaData(firstArg, 'Workflow', workflowName, logger, checker);
@@ -192,10 +191,6 @@ export const addWorkflow = (logger, node, checker, state) => {
192
191
  summary = metadata.summary;
193
192
  description = metadata.description;
194
193
  errors = metadata.errors;
195
- const inlineProp = getPropertyValue(firstArg, 'inline');
196
- if (inlineProp === true) {
197
- inline = true;
198
- }
199
194
  expose = getPropertyValue(firstArg, 'expose');
200
195
  }
201
196
  // Validate that we got a valid function
@@ -279,7 +274,6 @@ export const addWorkflow = (logger, node, checker, state) => {
279
274
  description,
280
275
  errors,
281
276
  tags,
282
- inline,
283
277
  expose,
284
278
  };
285
279
  // Workflow functions require platform services that aren't visible
@@ -17,8 +17,12 @@ export function findPiiPaths(checker, type, path = '', depth = 0, seen = new Set
17
17
  return [];
18
18
  seen.add(type);
19
19
  // ── Is this type itself branded? ─────────────────────────────────────────
20
- // Private<T> = T & { readonly __classification__: 'private' } → isIntersection()
21
- // where one constituent has a `__classification__` property whose type is a string literal.
20
+ // Private<T> = T & { readonly __classification__?: 'private' } → isIntersection()
21
+ // where one constituent has a `__classification__` property whose type is a
22
+ // string literal. The marker is OPTIONAL (so plain values stay assignable to
23
+ // branded columns), which means its resolved type is `'private' | undefined` —
24
+ // a union, not a bare literal. Read the level union-aware via `literalString`,
25
+ // otherwise pii/secret silently downgrade to the `'private'` fallback.
22
26
  if (type.isIntersection()) {
23
27
  for (const t of type.types) {
24
28
  const classificationProp = t
@@ -28,7 +32,7 @@ export function findPiiPaths(checker, type, path = '', depth = 0, seen = new Set
28
32
  const decl = classificationProp.valueDeclaration ??
29
33
  classificationProp.declarations?.[0];
30
34
  const classification = decl
31
- ? (checker.getTypeOfSymbolAtLocation(classificationProp, decl)?.value ?? 'private')
35
+ ? (literalString(checker.getTypeOfSymbolAtLocation(classificationProp, decl)) ?? 'private')
32
36
  : 'private';
33
37
  return [{ path: path || '<return value>', classification }];
34
38
  }
@@ -71,3 +75,22 @@ export function findPiiPaths(checker, type, path = '', depth = 0, seen = new Set
71
75
  }
72
76
  return violations;
73
77
  }
78
+ /**
79
+ * Recover a string-literal value from a type that may be the literal itself or a
80
+ * union containing it (e.g. `'private' | undefined`, produced by the optional
81
+ * `__classification__?` marker). Returns undefined when no string literal is
82
+ * present so the caller can apply its own fallback.
83
+ */
84
+ function literalString(type) {
85
+ const value = type.value;
86
+ if (typeof value === 'string')
87
+ return value;
88
+ if (type.isUnion()) {
89
+ for (const member of type.types) {
90
+ const found = literalString(member);
91
+ if (found !== undefined)
92
+ return found;
93
+ }
94
+ }
95
+ return undefined;
96
+ }
@@ -760,10 +760,15 @@ export function filterInspectorState(state, filters, logger) {
760
760
  }
761
761
  filteredState.requiredSchemas = prunedSchemas;
762
762
  }
763
- // If any surviving function is a non-inline workflow step, the unit needs
764
- // workflowService + queueService even though the function doesn't use them.
765
- // Check the ORIGINAL graph meta (before filtering pruned it).
763
+ // Step dispatch is decided purely per-function: a workflow step runs via the
764
+ // queue only when its function opts out of inline execution (inline: false).
765
+ // Such a unit needs workflowService + queueService injected even though the
766
+ // function itself doesn't reference them. Check the ORIGINAL graph meta
767
+ // (before filtering pruned it).
766
768
  const survivingFuncIds = new Set(Object.keys(filteredState.functions.meta));
769
+ const resolveFuncId = (rpcName) => filteredState.rpc.internalMeta[rpcName] ??
770
+ filteredState.rpc.exposedMeta[rpcName] ??
771
+ rpcName;
767
772
  // Use the snapshot taken before filtering
768
773
  for (const graph of Object.values(originalGraphMeta)) {
769
774
  if (!graph.nodes)
@@ -772,11 +777,12 @@ export function filterInspectorState(state, filters, logger) {
772
777
  if (!('rpcName' in node) || !node.rpcName)
773
778
  continue;
774
779
  const rpcName = node.rpcName;
775
- if (!survivingFuncIds.has(rpcName))
780
+ const funcId = resolveFuncId(rpcName);
781
+ if (!survivingFuncIds.has(funcId) && !survivingFuncIds.has(rpcName))
776
782
  continue;
777
- const isInline = node.options?.async !== true &&
778
- graph.inline === true;
779
- if (!isInline) {
783
+ const funcMeta = (filteredState.functions.meta[funcId] ??
784
+ filteredState.functions.meta[rpcName]);
785
+ if (funcMeta?.inline === false) {
780
786
  filteredState.serviceAggregation.requiredServices.add('workflowService');
781
787
  filteredState.serviceAggregation.requiredServices.add('queueService');
782
788
  }
@@ -301,7 +301,6 @@ export function convertDslToGraph(workflowName, meta) {
301
301
  source,
302
302
  description: meta.description,
303
303
  tags: meta.tags,
304
- inline: meta.inline,
305
304
  context: meta.context,
306
305
  nodes: nodesRecord,
307
306
  entryNodeIds,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.17",
3
+ "version": "0.12.19",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
38
- "@pikku/core": "^0.12.28",
38
+ "@pikku/core": "^0.12.31",
39
39
  "path-to-regexp": "^8.3.0",
40
40
  "ts-json-schema-generator": "^2.5.0",
41
41
  "tsx": "^4.21.0",
@@ -316,3 +316,55 @@ describe('addFunctions implementationHash', () => {
316
316
  }
317
317
  })
318
318
  })
319
+
320
+ describe('pikkuChannelConnectionFunc generic mapping', () => {
321
+ // Regression: pikkuChannelConnectionFunc<Out> has a single generic that is the
322
+ // OUTPUT type (input is always void). The inspector must NOT record that generic
323
+ // as inputSchemaName — otherwise the empty WS handshake is validated against an
324
+ // input schema requiring the send-payload shape and the connect is rejected 403.
325
+ test('does not map the output generic to inputSchemaName', async () => {
326
+ const rootDir = await mkdtemp(join(tmpdir(), 'pikku-channel-connect-'))
327
+ const file = join(rootDir, 'channel.ts')
328
+
329
+ await writeFile(
330
+ file,
331
+ [
332
+ 'type Sessionless<In, Out> = (',
333
+ ' services: any,',
334
+ ' data: In,',
335
+ ' interaction: any',
336
+ ') => Promise<Out>',
337
+ 'export const pikkuChannelConnectionFunc = <Out = unknown>(',
338
+ ' func: Sessionless<void, Out>',
339
+ ') => ({ func })',
340
+ 'export const onCardsConnect = pikkuChannelConnectionFunc<{',
341
+ " type: 'hello'",
342
+ ' count: number',
343
+ '}>(async (_services, _data, _interaction) => {})',
344
+ ].join('\n')
345
+ )
346
+
347
+ const logger: InspectorLogger = {
348
+ debug: () => {},
349
+ info: () => {},
350
+ warn: () => {},
351
+ error: () => {},
352
+ critical: () => {},
353
+ hasCriticalErrors: () => false,
354
+ }
355
+
356
+ try {
357
+ const state = await inspect(logger, [file], { rootDir })
358
+ const meta = state.functions.meta['onCardsConnect']
359
+ assert.ok(meta, 'onCardsConnect meta should exist')
360
+ assert.strictEqual(
361
+ meta!.inputSchemaName,
362
+ null,
363
+ 'connect input must be void (no input schema), not the output generic'
364
+ )
365
+ assert.deepStrictEqual(meta!.inputs, [])
366
+ } finally {
367
+ await rm(rootDir, { recursive: true, force: true })
368
+ }
369
+ })
370
+ })
@@ -671,6 +671,13 @@ export const addFunctions: AddWiring = (
671
671
  .map((tn) => checker.getTypeFromTypeNode(tn))
672
672
  .map((t) => unwrapPromise(checker, t))
673
673
 
674
+ // pikkuChannelConnectionFunc<Out> declares a single generic that is the
675
+ // OUTPUT type — its input is always void (PikkuFunctionSessionless<void, Out>).
676
+ // Every other wrapper reads generic[0] as INPUT, so without this guard the
677
+ // connect handler's output generic is mis-recorded as inputSchemaName and the
678
+ // empty WS handshake fails input validation at connect (1008/403).
679
+ const isChannelConnectionFunc = /ChannelConnection/i.test(expression.text)
680
+
674
681
  const capitalizedName = funcIdToTypeName(name)
675
682
 
676
683
  // --- Input Extraction ---
@@ -708,7 +715,12 @@ export const addFunctions: AddWiring = (
708
715
  } else {
709
716
  inputTypes = [filterType]
710
717
  }
711
- } else if (!isListFunc && genericTypes.length >= 1 && genericTypes[0]) {
718
+ } else if (
719
+ !isChannelConnectionFunc &&
720
+ !isListFunc &&
721
+ genericTypes.length >= 1 &&
722
+ genericTypes[0]
723
+ ) {
712
724
  // Fall back to extracting from generic type arguments
713
725
  const result = getNamesAndTypes(
714
726
  checker,
@@ -6,6 +6,7 @@ import {
6
6
  import type { AddWiring, InspectorState } from '../types.js'
7
7
  import { ErrorCode } from '../error-codes.js'
8
8
  import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
9
+ import { parseDurationString } from '@pikku/core'
9
10
 
10
11
  export interface KeyedWiringConfig {
11
12
  functionName: string
@@ -52,6 +53,9 @@ export const createAddKeyedWiring = (config: KeyedWiringConfig): AddWiring => {
52
53
  const descriptionValue = getPropertyValue(obj, 'description') as
53
54
  | string
54
55
  | null
56
+ const rotationPeriodValue = getPropertyValue(obj, 'rotationPeriod') as
57
+ | string
58
+ | null
55
59
  const idValue = getPropertyValue(obj, config.idField) as string | null
56
60
 
57
61
  let schemaVariableName: string | null = null
@@ -123,6 +127,18 @@ export const createAddKeyedWiring = (config: KeyedWiringConfig): AddWiring => {
123
127
  return
124
128
  }
125
129
 
130
+ if (rotationPeriodValue) {
131
+ try {
132
+ parseDurationString(rotationPeriodValue)
133
+ } catch {
134
+ logger.critical(
135
+ ErrorCode.INVALID_VALUE,
136
+ `${config.label} '${nameValue}' has an invalid 'rotationPeriod': '${rotationPeriodValue}'. Use a duration like '1d', '30day', or '1w'.`
137
+ )
138
+ return
139
+ }
140
+ }
141
+
126
142
  const sourceFile = node.getSourceFile().fileName
127
143
 
128
144
  const wiringState = config.getState(state)
@@ -148,6 +164,7 @@ export const createAddKeyedWiring = (config: KeyedWiringConfig): AddWiring => {
148
164
  name: nameValue,
149
165
  displayName: displayNameValue,
150
166
  description: descriptionValue || undefined,
167
+ rotationPeriod: rotationPeriodValue || undefined,
151
168
  [config.idField]: idValue,
152
169
  schema: schemaLookupName,
153
170
  sourceFile,
@@ -209,7 +209,6 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
209
209
  let summary: string | undefined
210
210
  let description: string | undefined
211
211
  let errors: string[] | undefined
212
- let inline: boolean | undefined
213
212
  let expose: boolean | undefined
214
213
 
215
214
  if (ts.isObjectLiteralExpression(firstArg)) {
@@ -226,11 +225,6 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
226
225
  description = metadata.description
227
226
  errors = metadata.errors
228
227
 
229
- const inlineProp = getPropertyValue(firstArg, 'inline')
230
- if (inlineProp === true) {
231
- inline = true
232
- }
233
-
234
228
  expose = getPropertyValue(firstArg, 'expose') as boolean | undefined
235
229
  }
236
230
 
@@ -337,7 +331,6 @@ export const addWorkflow: AddWiring = (logger, node, checker, state) => {
337
331
  description,
338
332
  errors,
339
333
  tags,
340
- inline,
341
334
  expose,
342
335
  }
343
336
 
@@ -27,10 +27,15 @@ function makeLogger() {
27
27
  * Mirrors what schema.d.ts emits so the TypeScript program sees the correct
28
28
  * structural brand type even without @pikku/core being importable from /tmp.
29
29
  */
30
+ // Optional `__classification__?` mirrors what @pikku/core and `pikku db migrate`
31
+ // actually emit (optional so plain values stay assignable to branded columns).
32
+ // The `Secret`-in-sessioned-function cases below double as the level-fidelity
33
+ // guard: they only pass if `findPiiPaths` reads the level union-aware
34
+ // ('secret' | undefined), not via a naive `.value`.
30
35
  const BRAND_TYPES = `
31
- type Private<T> = T & { readonly __classification__: 'private' }
32
- type Pii<T> = T & { readonly __classification__: 'pii' }
33
- type Secret<T> = T & { readonly __classification__: 'secret' }
36
+ type Private<T> = T & { readonly __classification__?: 'private' }
37
+ type Pii<T> = T & { readonly __classification__?: 'pii' }
38
+ type Secret<T> = T & { readonly __classification__?: 'secret' }
34
39
  `
35
40
 
36
41
  async function runInspect(sourceCode: string) {
@@ -29,8 +29,12 @@ export function findPiiPaths(
29
29
  seen.add(type)
30
30
 
31
31
  // ── Is this type itself branded? ─────────────────────────────────────────
32
- // Private<T> = T & { readonly __classification__: 'private' } → isIntersection()
33
- // where one constituent has a `__classification__` property whose type is a string literal.
32
+ // Private<T> = T & { readonly __classification__?: 'private' } → isIntersection()
33
+ // where one constituent has a `__classification__` property whose type is a
34
+ // string literal. The marker is OPTIONAL (so plain values stay assignable to
35
+ // branded columns), which means its resolved type is `'private' | undefined` —
36
+ // a union, not a bare literal. Read the level union-aware via `literalString`,
37
+ // otherwise pii/secret silently downgrade to the `'private'` fallback.
34
38
  if (type.isIntersection()) {
35
39
  for (const t of type.types) {
36
40
  const classificationProp = t
@@ -41,9 +45,9 @@ export function findPiiPaths(
41
45
  classificationProp.valueDeclaration ??
42
46
  classificationProp.declarations?.[0]
43
47
  const classification = decl
44
- ? ((
45
- checker.getTypeOfSymbolAtLocation(classificationProp, decl) as any
46
- )?.value ?? 'private')
48
+ ? (literalString(
49
+ checker.getTypeOfSymbolAtLocation(classificationProp, decl)
50
+ ) ?? 'private')
47
51
  : 'private'
48
52
  return [{ path: path || '<return value>', classification }]
49
53
  }
@@ -96,3 +100,21 @@ export function findPiiPaths(
96
100
 
97
101
  return violations
98
102
  }
103
+
104
+ /**
105
+ * Recover a string-literal value from a type that may be the literal itself or a
106
+ * union containing it (e.g. `'private' | undefined`, produced by the optional
107
+ * `__classification__?` marker). Returns undefined when no string literal is
108
+ * present so the caller can apply its own fallback.
109
+ */
110
+ function literalString(type: ts.Type): string | undefined {
111
+ const value = (type as { value?: unknown }).value
112
+ if (typeof value === 'string') return value
113
+ if (type.isUnion()) {
114
+ for (const member of type.types) {
115
+ const found = literalString(member)
116
+ if (found !== undefined) return found
117
+ }
118
+ }
119
+ return undefined
120
+ }
@@ -1061,21 +1061,28 @@ export function filterInspectorState(
1061
1061
  filteredState.requiredSchemas = prunedSchemas
1062
1062
  }
1063
1063
 
1064
- // If any surviving function is a non-inline workflow step, the unit needs
1065
- // workflowService + queueService even though the function doesn't use them.
1066
- // Check the ORIGINAL graph meta (before filtering pruned it).
1064
+ // Step dispatch is decided purely per-function: a workflow step runs via the
1065
+ // queue only when its function opts out of inline execution (inline: false).
1066
+ // Such a unit needs workflowService + queueService injected even though the
1067
+ // function itself doesn't reference them. Check the ORIGINAL graph meta
1068
+ // (before filtering pruned it).
1067
1069
  const survivingFuncIds = new Set(Object.keys(filteredState.functions.meta))
1070
+ const resolveFuncId = (rpcName: string): string =>
1071
+ filteredState.rpc.internalMeta[rpcName] ??
1072
+ filteredState.rpc.exposedMeta[rpcName] ??
1073
+ rpcName
1068
1074
  // Use the snapshot taken before filtering
1069
1075
  for (const graph of Object.values(originalGraphMeta)) {
1070
1076
  if (!graph.nodes) continue
1071
1077
  for (const node of Object.values(graph.nodes)) {
1072
1078
  if (!('rpcName' in node) || !node.rpcName) continue
1073
1079
  const rpcName = node.rpcName as string
1074
- if (!survivingFuncIds.has(rpcName)) continue
1075
- const isInline =
1076
- (node as { options?: { async?: boolean } }).options?.async !== true &&
1077
- graph.inline === true
1078
- if (!isInline) {
1080
+ const funcId = resolveFuncId(rpcName)
1081
+ if (!survivingFuncIds.has(funcId) && !survivingFuncIds.has(rpcName))
1082
+ continue
1083
+ const funcMeta = (filteredState.functions.meta[funcId] ??
1084
+ filteredState.functions.meta[rpcName]) as { inline?: boolean }
1085
+ if (funcMeta?.inline === false) {
1079
1086
  filteredState.serviceAggregation.requiredServices.add('workflowService')
1080
1087
  filteredState.serviceAggregation.requiredServices.add('queueService')
1081
1088
  }
@@ -400,7 +400,6 @@ export function convertDslToGraph(
400
400
  source,
401
401
  description: meta.description,
402
402
  tags: meta.tags,
403
- inline: meta.inline,
404
403
  context: meta.context,
405
404
  nodes: nodesRecord,
406
405
  entryNodeIds,