@pikku/inspector 0.12.16 → 0.12.18
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 +39 -0
- package/dist/add/add-functions.js +13 -1
- package/dist/add/add-keyed-wiring.js +12 -0
- package/dist/add/add-workflow.js +0 -6
- package/dist/utils/custom-types-generator.js +55 -4
- package/dist/utils/filter-inspector-state.js +13 -7
- package/dist/utils/schema-generator.js +4 -2
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +23 -0
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +0 -1
- package/package.json +2 -2
- package/src/add/add-functions.test.ts +52 -0
- package/src/add/add-functions.ts +16 -1
- package/src/add/add-keyed-wiring.ts +17 -0
- package/src/add/add-workflow.ts +0 -7
- package/src/utils/custom-types-generator.test.ts +99 -0
- package/src/utils/custom-types-generator.ts +64 -4
- package/src/utils/filter-inspector-state.ts +15 -8
- package/src/utils/schema-generator.ts +4 -2
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +28 -2
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
## 0.12.18
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 20750fd: feat(workflow): decide step dispatch purely per-function
|
|
6
|
+
|
|
7
|
+
Workflow step execution (inline vs queue dispatch) is now decided entirely by
|
|
8
|
+
the step's function `inline` flag — the workflow-level / run-level `inline`
|
|
9
|
+
meta no longer participates in per-step dispatch.
|
|
10
|
+
- Steps default to **inline**, so a normally-started (queue-backed) workflow
|
|
11
|
+
runs its whole chain in one orchestrator pass instead of one queue
|
|
12
|
+
round-trip per step.
|
|
13
|
+
- A function marked `inline: false` is dispatched via the queue (its own
|
|
14
|
+
worker, retry isolation). When `inline: false` but no `queueService` is
|
|
15
|
+
configured, the step falls back to inline and emits a `logger.warn` instead
|
|
16
|
+
of silently swallowing the misconfiguration.
|
|
17
|
+
- Removed the now-unused workflow-level `inline` from `WorkflowsMeta` /
|
|
18
|
+
`WorkflowRuntimeMeta`, the inspector's workflow extraction, the DSL→graph
|
|
19
|
+
converter, and the deploy analyzer / service inference (which now key off
|
|
20
|
+
the per-function flag). Run-level `inline` is retained: it still controls
|
|
21
|
+
whether a whole run executes in-process without queue infrastructure.
|
|
22
|
+
|
|
23
|
+
- Updated dependencies [cd101a5]
|
|
24
|
+
- Updated dependencies [ac16265]
|
|
25
|
+
- Updated dependencies [a05e864]
|
|
26
|
+
- Updated dependencies [20750fd]
|
|
27
|
+
- @pikku/core@0.12.30
|
|
28
|
+
|
|
29
|
+
## 0.12.17
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- 2cf67be: Add inline option to pikkuFunc/pikkuSessionlessFunc for workflow step dispatch
|
|
34
|
+
|
|
35
|
+
By default, workflow steps now run inline (no queue hop). Set inline: false on a function to force dispatch through the queue for that step.
|
|
36
|
+
|
|
37
|
+
- Updated dependencies [2cf67be]
|
|
38
|
+
- @pikku/core@0.12.28
|
|
39
|
+
|
|
1
40
|
## 0.12.16
|
|
2
41
|
|
|
3
42
|
### Patch Changes
|
|
@@ -277,6 +277,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
277
277
|
let deploy;
|
|
278
278
|
let approvalRequired;
|
|
279
279
|
let approvalDescription;
|
|
280
|
+
let inline;
|
|
280
281
|
let version;
|
|
281
282
|
let objectNode;
|
|
282
283
|
let nodeDisplayName = null;
|
|
@@ -344,6 +345,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
344
345
|
readonly_ = getPropertyValue(firstArg, 'readonly');
|
|
345
346
|
deploy = getPropertyValue(firstArg, 'deploy');
|
|
346
347
|
approvalRequired = getPropertyValue(firstArg, 'approvalRequired');
|
|
348
|
+
inline = getPropertyValue(firstArg, 'inline');
|
|
347
349
|
// Extract approvalDescription identifier reference
|
|
348
350
|
for (const prop of firstArg.properties) {
|
|
349
351
|
if (ts.isPropertyAssignment(prop) &&
|
|
@@ -486,6 +488,12 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
486
488
|
const genericTypes = (typeArguments ?? [])
|
|
487
489
|
.map((tn) => checker.getTypeFromTypeNode(tn))
|
|
488
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);
|
|
489
497
|
const capitalizedName = funcIdToTypeName(name);
|
|
490
498
|
// --- Input Extraction ---
|
|
491
499
|
let inputNames = [];
|
|
@@ -511,7 +519,10 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
511
519
|
inputTypes = [filterType];
|
|
512
520
|
}
|
|
513
521
|
}
|
|
514
|
-
else if (!
|
|
522
|
+
else if (!isChannelConnectionFunc &&
|
|
523
|
+
!isListFunc &&
|
|
524
|
+
genericTypes.length >= 1 &&
|
|
525
|
+
genericTypes[0]) {
|
|
515
526
|
// Fall back to extracting from generic type arguments
|
|
516
527
|
const result = getNamesAndTypes(checker, state.functions.typesMap, 'Input', name, genericTypes[0]);
|
|
517
528
|
inputNames = result.names;
|
|
@@ -716,6 +727,7 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
716
727
|
deploy: deploy || undefined,
|
|
717
728
|
approvalRequired: approvalRequired || undefined,
|
|
718
729
|
approvalDescription: approvalDescription || undefined,
|
|
730
|
+
inline: inline === false ? false : undefined,
|
|
719
731
|
implementationHash,
|
|
720
732
|
version,
|
|
721
733
|
title,
|
|
@@ -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,
|
package/dist/add/add-workflow.js
CHANGED
|
@@ -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
|
|
@@ -7,6 +7,53 @@
|
|
|
7
7
|
export function sanitizeTypeName(name) {
|
|
8
8
|
return name.replace(/[^a-zA-Z0-9_$]/g, '_');
|
|
9
9
|
}
|
|
10
|
+
const CLASSIFICATION_WRAPPERS = new Set(['Private', 'Pii', 'Secret']);
|
|
11
|
+
function findMatchingAngleBracket(type, startIndex) {
|
|
12
|
+
let depth = 0;
|
|
13
|
+
for (let i = startIndex; i < type.length; i += 1) {
|
|
14
|
+
const char = type[i];
|
|
15
|
+
if (char === '<') {
|
|
16
|
+
depth += 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (char === '>' && (i === 0 || type[i - 1] !== '=')) {
|
|
20
|
+
depth -= 1;
|
|
21
|
+
if (depth === 0) {
|
|
22
|
+
return i;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return -1;
|
|
27
|
+
}
|
|
28
|
+
function stripClassificationWrappers(type) {
|
|
29
|
+
let output = '';
|
|
30
|
+
let index = 0;
|
|
31
|
+
while (index < type.length) {
|
|
32
|
+
const char = type[index];
|
|
33
|
+
if (!/[A-Za-z_$]/.test(char)) {
|
|
34
|
+
output += char;
|
|
35
|
+
index += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
let end = index + 1;
|
|
39
|
+
while (end < type.length && /[A-Za-z0-9_$]/.test(type[end])) {
|
|
40
|
+
end += 1;
|
|
41
|
+
}
|
|
42
|
+
const identifier = type.slice(index, end);
|
|
43
|
+
if (CLASSIFICATION_WRAPPERS.has(identifier) && type[end] === '<') {
|
|
44
|
+
const closingIndex = findMatchingAngleBracket(type, end);
|
|
45
|
+
if (closingIndex !== -1) {
|
|
46
|
+
const inner = type.slice(end + 1, closingIndex);
|
|
47
|
+
output += stripClassificationWrappers(inner);
|
|
48
|
+
index = closingIndex + 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
output += identifier;
|
|
53
|
+
index = end;
|
|
54
|
+
}
|
|
55
|
+
return output;
|
|
56
|
+
}
|
|
10
57
|
export function generateCustomTypes(typesMap, requiredTypes) {
|
|
11
58
|
const typeDeclarations = Array.from(typesMap.customTypes.entries())
|
|
12
59
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
@@ -17,11 +64,13 @@ export function generateCustomTypes(typesMap, requiredTypes) {
|
|
|
17
64
|
.map(([originalName, { type, references }]) => {
|
|
18
65
|
const name = sanitizeTypeName(originalName);
|
|
19
66
|
references.forEach((refName) => {
|
|
20
|
-
if (refName !== '__object' &&
|
|
67
|
+
if (refName !== '__object' &&
|
|
68
|
+
!refName.startsWith('__object_') &&
|
|
69
|
+
!CLASSIFICATION_WRAPPERS.has(refName)) {
|
|
21
70
|
requiredTypes.add(refName);
|
|
22
71
|
}
|
|
23
72
|
});
|
|
24
|
-
const typeString = type;
|
|
73
|
+
const typeString = stripClassificationWrappers(type);
|
|
25
74
|
const typeNameRegex = /\b[A-Z][a-zA-Z0-9]*\b/g;
|
|
26
75
|
const potentialTypes = typeString.match(typeNameRegex) || [];
|
|
27
76
|
potentialTypes.forEach((typeName) => {
|
|
@@ -46,12 +95,14 @@ export function generateCustomTypes(typesMap, requiredTypes) {
|
|
|
46
95
|
// Type not found in map (ambient/builtin type)
|
|
47
96
|
}
|
|
48
97
|
});
|
|
49
|
-
if (name ===
|
|
98
|
+
if (name === typeString)
|
|
50
99
|
return null;
|
|
51
|
-
return `export type ${name} = ${
|
|
100
|
+
return `export type ${name} = ${typeString}`;
|
|
52
101
|
});
|
|
53
102
|
const importsByPath = new Map();
|
|
54
103
|
for (const typeName of requiredTypes) {
|
|
104
|
+
if (CLASSIFICATION_WRAPPERS.has(typeName))
|
|
105
|
+
continue;
|
|
55
106
|
try {
|
|
56
107
|
const typeMeta = typesMap.getTypeMeta(typeName);
|
|
57
108
|
if (typeMeta.path) {
|
|
@@ -760,10 +760,15 @@ export function filterInspectorState(state, filters, logger) {
|
|
|
760
760
|
}
|
|
761
761
|
filteredState.requiredSchemas = prunedSchemas;
|
|
762
762
|
}
|
|
763
|
-
//
|
|
764
|
-
//
|
|
765
|
-
//
|
|
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
|
-
|
|
780
|
+
const funcId = resolveFuncId(rpcName);
|
|
781
|
+
if (!survivingFuncIds.has(funcId) && !survivingFuncIds.has(rpcName))
|
|
776
782
|
continue;
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
if (
|
|
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
|
}
|
|
@@ -212,7 +212,9 @@ async function batchImportWithRegister(logger, sourceFiles) {
|
|
|
212
212
|
return null;
|
|
213
213
|
}
|
|
214
214
|
finally {
|
|
215
|
-
|
|
215
|
+
void Promise.resolve(unregister?.()).catch((e) => {
|
|
216
|
+
logger.debug(`tsx unregister() failed: ${e.message}`);
|
|
217
|
+
});
|
|
216
218
|
}
|
|
217
219
|
}
|
|
218
220
|
async function importWithRegister(sourceFile) {
|
|
@@ -221,7 +223,7 @@ async function importWithRegister(sourceFile) {
|
|
|
221
223
|
return await import(sourceFile);
|
|
222
224
|
}
|
|
223
225
|
finally {
|
|
224
|
-
|
|
226
|
+
void Promise.resolve(unregister()).catch(() => { });
|
|
225
227
|
}
|
|
226
228
|
}
|
|
227
229
|
function processZodSchema(schemaName, zodSchema, schemas, typesMap, auxiliaryTypeStore, printer, fakeSourceFile, logger) {
|
|
@@ -1040,6 +1040,29 @@ function extractReturn(statement, context) {
|
|
|
1040
1040
|
if (!statement.expression) {
|
|
1041
1041
|
return null;
|
|
1042
1042
|
}
|
|
1043
|
+
if (ts.isAwaitExpression(statement.expression) &&
|
|
1044
|
+
ts.isCallExpression(statement.expression.expression)) {
|
|
1045
|
+
const call = statement.expression.expression;
|
|
1046
|
+
if (isWorkflowDoCall(call, context.checker)) {
|
|
1047
|
+
return isInlineDoCall(call)
|
|
1048
|
+
? extractInlineStep(call, context)
|
|
1049
|
+
: extractRpcStep(call, context);
|
|
1050
|
+
}
|
|
1051
|
+
if (isWorkflowSleepCall(call, context.checker)) {
|
|
1052
|
+
return extractSleepStep(call, context);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (ts.isCallExpression(statement.expression)) {
|
|
1056
|
+
const call = statement.expression;
|
|
1057
|
+
if (isWorkflowDoCall(call, context.checker)) {
|
|
1058
|
+
return isInlineDoCall(call)
|
|
1059
|
+
? extractInlineStep(call, context)
|
|
1060
|
+
: extractRpcStep(call, context);
|
|
1061
|
+
}
|
|
1062
|
+
if (isWorkflowSleepCall(call, context.checker)) {
|
|
1063
|
+
return extractSleepStep(call, context);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1043
1066
|
if (!ts.isObjectLiteralExpression(statement.expression)) {
|
|
1044
1067
|
return null;
|
|
1045
1068
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.18",
|
|
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.
|
|
38
|
+
"@pikku/core": "^0.12.30",
|
|
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
|
+
})
|
package/src/add/add-functions.ts
CHANGED
|
@@ -392,6 +392,7 @@ export const addFunctions: AddWiring = (
|
|
|
392
392
|
let deploy: 'serverless' | 'server' | 'auto' | undefined
|
|
393
393
|
let approvalRequired: boolean | undefined
|
|
394
394
|
let approvalDescription: string | undefined
|
|
395
|
+
let inline: boolean | undefined
|
|
395
396
|
let version: number | undefined
|
|
396
397
|
let objectNode: ts.ObjectLiteralExpression | undefined
|
|
397
398
|
let nodeDisplayName: string | null = null
|
|
@@ -487,6 +488,7 @@ export const addFunctions: AddWiring = (
|
|
|
487
488
|
approvalRequired = getPropertyValue(firstArg, 'approvalRequired') as
|
|
488
489
|
| boolean
|
|
489
490
|
| undefined
|
|
491
|
+
inline = getPropertyValue(firstArg, 'inline') as boolean | undefined
|
|
490
492
|
|
|
491
493
|
// Extract approvalDescription identifier reference
|
|
492
494
|
for (const prop of firstArg.properties) {
|
|
@@ -669,6 +671,13 @@ export const addFunctions: AddWiring = (
|
|
|
669
671
|
.map((tn) => checker.getTypeFromTypeNode(tn))
|
|
670
672
|
.map((t) => unwrapPromise(checker, t))
|
|
671
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
|
+
|
|
672
681
|
const capitalizedName = funcIdToTypeName(name)
|
|
673
682
|
|
|
674
683
|
// --- Input Extraction ---
|
|
@@ -706,7 +715,12 @@ export const addFunctions: AddWiring = (
|
|
|
706
715
|
} else {
|
|
707
716
|
inputTypes = [filterType]
|
|
708
717
|
}
|
|
709
|
-
} else if (
|
|
718
|
+
} else if (
|
|
719
|
+
!isChannelConnectionFunc &&
|
|
720
|
+
!isListFunc &&
|
|
721
|
+
genericTypes.length >= 1 &&
|
|
722
|
+
genericTypes[0]
|
|
723
|
+
) {
|
|
710
724
|
// Fall back to extracting from generic type arguments
|
|
711
725
|
const result = getNamesAndTypes(
|
|
712
726
|
checker,
|
|
@@ -1004,6 +1018,7 @@ export const addFunctions: AddWiring = (
|
|
|
1004
1018
|
deploy: deploy || undefined,
|
|
1005
1019
|
approvalRequired: approvalRequired || undefined,
|
|
1006
1020
|
approvalDescription: approvalDescription || undefined,
|
|
1021
|
+
inline: inline === false ? false : undefined,
|
|
1007
1022
|
implementationHash,
|
|
1008
1023
|
version,
|
|
1009
1024
|
title,
|
|
@@ -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,
|
package/src/add/add-workflow.ts
CHANGED
|
@@ -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
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { test, describe } from 'node:test'
|
|
2
|
+
import { strict as assert } from 'node:assert'
|
|
3
|
+
import { generateCustomTypes } from './custom-types-generator.js'
|
|
4
|
+
import { TypesMap } from '../types-map.js'
|
|
5
|
+
|
|
6
|
+
function makeTypesMap(
|
|
7
|
+
entries: Record<string, { type: string; references: string[] }>
|
|
8
|
+
): TypesMap {
|
|
9
|
+
const tm = new TypesMap()
|
|
10
|
+
for (const [name, { type, references }] of Object.entries(entries)) {
|
|
11
|
+
tm.addCustomType(name, type, references)
|
|
12
|
+
}
|
|
13
|
+
return tm
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('generateCustomTypes — classification wrapper stripping', () => {
|
|
17
|
+
test('strips Private<T> wrapper from type alias', () => {
|
|
18
|
+
const tm = makeTypesMap({
|
|
19
|
+
UserEmail: { type: 'Private<string>', references: ['Private'] },
|
|
20
|
+
})
|
|
21
|
+
const result = generateCustomTypes(tm, new Set())
|
|
22
|
+
assert.match(result, /UserEmail/, 'should emit UserEmail alias')
|
|
23
|
+
assert.match(
|
|
24
|
+
result,
|
|
25
|
+
/= string/,
|
|
26
|
+
'Private<string> should be stripped to string'
|
|
27
|
+
)
|
|
28
|
+
assert.doesNotMatch(result, /Private/, 'Private wrapper must be removed')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('strips Secret<T> wrapper from type alias', () => {
|
|
32
|
+
const tm = makeTypesMap({
|
|
33
|
+
HashedPw: { type: 'Secret<string>', references: ['Secret'] },
|
|
34
|
+
})
|
|
35
|
+
const result = generateCustomTypes(tm, new Set())
|
|
36
|
+
assert.match(result, /HashedPw/)
|
|
37
|
+
assert.match(
|
|
38
|
+
result,
|
|
39
|
+
/= string/,
|
|
40
|
+
'Secret<string> should be stripped to string'
|
|
41
|
+
)
|
|
42
|
+
assert.doesNotMatch(result, /Secret/, 'Secret wrapper must be removed')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('strips Pii<T> wrapper from type alias', () => {
|
|
46
|
+
const tm = makeTypesMap({
|
|
47
|
+
UserPhone: { type: 'Pii<string>', references: ['Pii'] },
|
|
48
|
+
})
|
|
49
|
+
const result = generateCustomTypes(tm, new Set())
|
|
50
|
+
assert.match(result, /UserPhone/)
|
|
51
|
+
assert.match(result, /= string/, 'Pii<string> should be stripped to string')
|
|
52
|
+
assert.doesNotMatch(result, /Pii/, 'Pii wrapper must be removed')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('strips nested classification wrappers', () => {
|
|
56
|
+
const tm = makeTypesMap({
|
|
57
|
+
Combo: {
|
|
58
|
+
type: 'Private<Secret<string>>',
|
|
59
|
+
references: ['Private', 'Secret'],
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
const result = generateCustomTypes(tm, new Set())
|
|
63
|
+
assert.match(result, /Combo/)
|
|
64
|
+
assert.match(
|
|
65
|
+
result,
|
|
66
|
+
/= string/,
|
|
67
|
+
'nested wrappers should resolve to inner type'
|
|
68
|
+
)
|
|
69
|
+
assert.doesNotMatch(
|
|
70
|
+
result,
|
|
71
|
+
/Private|Secret/,
|
|
72
|
+
'all wrappers must be removed'
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('does not strip non-classification type names', () => {
|
|
77
|
+
const tm = makeTypesMap({
|
|
78
|
+
MyType: { type: 'SomeOtherType', references: ['SomeOtherType'] },
|
|
79
|
+
})
|
|
80
|
+
const required = new Set<string>()
|
|
81
|
+
generateCustomTypes(tm, required)
|
|
82
|
+
assert.ok(
|
|
83
|
+
required.has('SomeOtherType'),
|
|
84
|
+
'non-classification references must remain in requiredTypes'
|
|
85
|
+
)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('classification wrapper references are not added to requiredTypes', () => {
|
|
89
|
+
const tm = makeTypesMap({
|
|
90
|
+
SensitiveField: { type: 'Private<string>', references: ['Private'] },
|
|
91
|
+
})
|
|
92
|
+
const required = new Set<string>()
|
|
93
|
+
generateCustomTypes(tm, required)
|
|
94
|
+
assert.ok(
|
|
95
|
+
!required.has('Private'),
|
|
96
|
+
'Private must not be added to requiredTypes'
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
})
|