@pikku/inspector 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -1
- package/OPTIMIZATION-PLAN.md +195 -0
- package/dist/add/add-ai-agent.d.ts +2 -0
- package/dist/add/add-ai-agent.js +314 -0
- package/dist/add/add-channel.js +69 -61
- package/dist/add/add-cli.js +36 -18
- package/dist/add/add-file-with-factory.js +2 -0
- package/dist/add/add-functions.js +327 -59
- package/dist/add/add-http-route.d.ts +19 -10
- package/dist/add/add-http-route.js +153 -44
- package/dist/add/add-http-routes.d.ts +5 -0
- package/dist/add/add-http-routes.js +159 -0
- package/dist/add/add-keyed-wiring.d.ts +12 -0
- package/dist/add/add-keyed-wiring.js +97 -0
- package/dist/add/add-mcp-prompt.js +14 -9
- package/dist/add/add-mcp-resource.js +14 -9
- package/dist/add/add-middleware.d.ts +1 -4
- package/dist/add/add-middleware.js +364 -79
- package/dist/add/add-permission.d.ts +1 -1
- package/dist/add/add-permission.js +152 -40
- package/dist/add/add-queue-worker.js +18 -12
- package/dist/add/add-rpc-invocations.d.ts +3 -0
- package/dist/add/add-rpc-invocations.js +65 -25
- package/dist/add/add-schedule.js +11 -5
- package/dist/add/add-secret.d.ts +3 -0
- package/dist/add/add-secret.js +82 -0
- package/dist/add/add-trigger.d.ts +2 -0
- package/dist/add/add-trigger.js +87 -0
- package/dist/add/add-variable.d.ts +1 -0
- package/dist/add/add-variable.js +8 -0
- package/dist/add/add-workflow-graph.d.ts +7 -0
- package/dist/add/add-workflow-graph.js +396 -0
- package/dist/add/add-workflow.js +124 -26
- package/dist/error-codes.d.ts +16 -1
- package/dist/error-codes.js +21 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.js +5 -2
- package/dist/inspector.d.ts +1 -1
- package/dist/inspector.js +106 -13
- package/dist/schema-generator.d.ts +1 -0
- package/dist/schema-generator.js +1 -0
- package/dist/types-map.js +10 -1
- package/dist/types.d.ts +180 -30
- package/dist/utils/compute-required-schemas.d.ts +4 -0
- package/dist/utils/compute-required-schemas.js +41 -0
- package/dist/utils/contract-hashes.d.ts +35 -0
- package/dist/utils/contract-hashes.js +202 -0
- package/dist/utils/custom-types-generator.d.ts +9 -0
- package/dist/utils/custom-types-generator.js +71 -0
- package/dist/utils/detect-schema-vendor.d.ts +22 -0
- package/dist/utils/detect-schema-vendor.js +76 -0
- package/dist/utils/ensure-function-metadata.d.ts +5 -2
- package/dist/utils/ensure-function-metadata.js +220 -6
- package/dist/utils/extract-function-name.d.ts +5 -16
- package/dist/utils/extract-function-name.js +93 -298
- package/dist/utils/extract-services.d.ts +2 -1
- package/dist/utils/extract-services.js +25 -1
- package/dist/utils/filter-inspector-state.js +107 -23
- package/dist/utils/get-property-value.d.ts +8 -2
- package/dist/utils/get-property-value.js +33 -4
- package/dist/utils/hash.d.ts +2 -0
- package/dist/utils/hash.js +23 -0
- package/dist/utils/middleware.d.ts +7 -30
- package/dist/utils/middleware.js +80 -66
- package/dist/utils/permissions.d.ts +2 -2
- package/dist/utils/permissions.js +10 -10
- package/dist/utils/post-process.d.ts +9 -10
- package/dist/utils/post-process.js +231 -24
- package/dist/utils/resolve-external-package.d.ts +12 -0
- package/dist/utils/resolve-external-package.js +34 -0
- package/dist/utils/resolve-function-types.d.ts +6 -0
- package/dist/utils/resolve-function-types.js +29 -0
- package/dist/utils/resolve-identifier.d.ts +10 -0
- package/dist/utils/resolve-identifier.js +36 -0
- package/dist/utils/resolve-versions.d.ts +2 -0
- package/dist/utils/resolve-versions.js +78 -0
- package/dist/utils/schema-generator.d.ts +9 -0
- package/dist/utils/schema-generator.js +209 -0
- package/dist/utils/serialize-inspector-state.d.ts +73 -13
- package/dist/utils/serialize-inspector-state.js +102 -6
- package/dist/utils/serialize-mcp-json.d.ts +2 -0
- package/dist/utils/serialize-mcp-json.js +99 -0
- package/dist/utils/serialize-middleware-groups-meta.d.ts +12 -0
- package/dist/utils/serialize-middleware-groups-meta.js +28 -0
- package/dist/utils/serialize-openapi-json.d.ts +85 -0
- package/dist/utils/serialize-openapi-json.js +151 -0
- package/dist/utils/serialize-permissions-groups-meta.d.ts +6 -0
- package/dist/utils/serialize-permissions-groups-meta.js +31 -0
- package/dist/utils/workflow/dsl/deserialize-dsl-workflow.d.ts +24 -0
- package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +830 -0
- package/dist/{workflow/extract-simple-workflow.d.ts → utils/workflow/dsl/extract-dsl-workflow.d.ts} +4 -2
- package/dist/{workflow/extract-simple-workflow.js → utils/workflow/dsl/extract-dsl-workflow.js} +572 -72
- package/dist/utils/workflow/dsl/index.d.ts +7 -0
- package/dist/utils/workflow/dsl/index.js +7 -0
- package/dist/{workflow → utils/workflow/dsl}/patterns.d.ts +21 -0
- package/dist/{workflow → utils/workflow/dsl}/patterns.js +90 -10
- package/dist/{workflow → utils/workflow/dsl}/validation.d.ts +2 -0
- package/dist/{workflow → utils/workflow/dsl}/validation.js +25 -7
- package/dist/utils/workflow/graph/convert-dsl-to-graph.d.ts +13 -0
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +318 -0
- package/dist/utils/workflow/graph/finalize-workflow-wires.d.ts +3 -0
- package/dist/utils/workflow/graph/finalize-workflow-wires.js +276 -0
- package/dist/utils/workflow/graph/finalize-workflows.d.ts +2 -0
- package/dist/utils/workflow/graph/finalize-workflows.js +75 -0
- package/dist/utils/workflow/graph/index.d.ts +8 -0
- package/dist/utils/workflow/graph/index.js +8 -0
- package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +35 -0
- package/dist/utils/workflow/graph/serialize-workflow-graph.js +150 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +203 -0
- package/dist/utils/workflow/graph/workflow-graph.types.js +38 -0
- package/dist/visit.js +13 -2
- package/package.json +26 -4
- package/src/add/add-ai-agent.ts +468 -0
- package/src/add/add-channel.ts +82 -79
- package/src/add/add-cli.ts +49 -20
- package/src/add/add-file-with-factory.ts +2 -0
- package/src/add/add-functions.ts +429 -71
- package/src/add/add-http-route.ts +246 -65
- package/src/add/add-http-routes.ts +228 -0
- package/src/add/add-keyed-wiring.ts +151 -0
- package/src/add/add-mcp-prompt.ts +26 -15
- package/src/add/add-mcp-resource.ts +27 -15
- package/src/add/add-middleware.ts +482 -80
- package/src/add/add-permission.ts +199 -40
- package/src/add/add-queue-worker.ts +24 -19
- package/src/add/add-rpc-invocations.ts +78 -31
- package/src/add/add-schedule.ts +16 -11
- package/src/add/add-secret.ts +140 -0
- package/src/add/add-trigger.ts +154 -0
- package/src/add/add-variable.ts +9 -0
- package/src/add/add-workflow-graph.ts +522 -0
- package/src/add/add-workflow.ts +117 -30
- package/src/error-codes.ts +26 -1
- package/src/index.ts +27 -8
- package/src/inspector.ts +145 -17
- package/src/schema-generator.ts +1 -0
- package/src/types-map.ts +12 -1
- package/src/types.ts +192 -51
- package/src/utils/compute-required-schemas.ts +49 -0
- package/src/utils/contract-hashes.test.ts +528 -0
- package/src/utils/contract-hashes.ts +290 -0
- package/src/utils/custom-types-generator.ts +88 -0
- package/src/utils/detect-schema-vendor.ts +90 -0
- package/src/utils/ensure-function-metadata.ts +324 -7
- package/src/utils/extract-function-name.ts +108 -358
- package/src/utils/extract-services.ts +35 -2
- package/src/utils/filter-inspector-state.test.ts +34 -20
- package/src/utils/filter-inspector-state.ts +140 -31
- package/src/utils/get-property-value.ts +50 -5
- package/src/utils/hash.ts +26 -0
- package/src/utils/middleware.test.ts +204 -0
- package/src/utils/middleware.ts +129 -67
- package/src/utils/permissions.test.ts +35 -12
- package/src/utils/permissions.ts +10 -10
- package/src/utils/post-process.ts +283 -43
- package/src/utils/resolve-external-package.ts +42 -0
- package/src/utils/resolve-function-types.ts +42 -0
- package/src/utils/resolve-identifier.ts +46 -0
- package/src/utils/resolve-versions.test.ts +249 -0
- package/src/utils/resolve-versions.ts +105 -0
- package/src/utils/schema-generator.ts +329 -0
- package/src/utils/serialize-inspector-state.ts +181 -20
- package/src/utils/serialize-mcp-json.ts +145 -0
- package/src/utils/serialize-middleware-groups-meta.ts +33 -0
- package/src/utils/serialize-openapi-json.ts +277 -0
- package/src/utils/serialize-permissions-groups-meta.ts +35 -0
- package/src/utils/test-data/inspector-state.json +69 -66
- package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +1104 -0
- package/src/{workflow/extract-simple-workflow.ts → utils/workflow/dsl/extract-dsl-workflow.ts} +678 -85
- package/src/utils/workflow/dsl/index.ts +11 -0
- package/src/{workflow → utils/workflow/dsl}/patterns.ts +108 -11
- package/src/{workflow → utils/workflow/dsl}/validation.ts +34 -7
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +422 -0
- package/src/utils/workflow/graph/finalize-workflow-wires.ts +310 -0
- package/src/utils/workflow/graph/finalize-workflows.ts +100 -0
- package/src/utils/workflow/graph/index.ts +11 -0
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +216 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +231 -0
- package/src/visit.ts +14 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/add/add-mcp-tool.d.ts +0 -2
- package/dist/add/add-mcp-tool.js +0 -81
- package/dist/utils/extract-service-metadata.d.ts +0 -19
- package/dist/utils/extract-service-metadata.js +0 -244
- package/dist/utils/write-service-metadata.d.ts +0 -13
- package/dist/utils/write-service-metadata.js +0 -37
- package/src/add/add-mcp-tool.ts +0 -141
- package/src/utils/extract-service-metadata.ts +0 -353
- package/src/utils/write-service-metadata.ts +0 -51
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { strict as assert } from 'assert'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
import { resolveLatestVersions } from './resolve-versions.js'
|
|
4
|
+
import type { InspectorState } from '../types.js'
|
|
5
|
+
|
|
6
|
+
function makeState(
|
|
7
|
+
meta: Record<string, any>,
|
|
8
|
+
rpc?: {
|
|
9
|
+
internalMeta?: Record<string, string>
|
|
10
|
+
internalFiles?: Map<string, any>
|
|
11
|
+
}
|
|
12
|
+
): InspectorState {
|
|
13
|
+
return {
|
|
14
|
+
functions: {
|
|
15
|
+
meta,
|
|
16
|
+
files: new Map(),
|
|
17
|
+
typesMap: {} as any,
|
|
18
|
+
},
|
|
19
|
+
rpc: {
|
|
20
|
+
internalMeta: rpc?.internalMeta ?? {},
|
|
21
|
+
internalFiles: rpc?.internalFiles ?? new Map(),
|
|
22
|
+
exposedMeta: {},
|
|
23
|
+
exposedFiles: new Map(),
|
|
24
|
+
invokedFunctions: new Set(),
|
|
25
|
+
usedExternalPackages: new Set(),
|
|
26
|
+
},
|
|
27
|
+
serviceAggregation: {
|
|
28
|
+
usedFunctions: new Set(),
|
|
29
|
+
requiredServices: new Set(),
|
|
30
|
+
usedMiddleware: new Set(),
|
|
31
|
+
usedPermissions: new Set(),
|
|
32
|
+
allSingletonServices: [],
|
|
33
|
+
allWireServices: [],
|
|
34
|
+
},
|
|
35
|
+
} as any
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeLogger() {
|
|
39
|
+
const errors: string[] = []
|
|
40
|
+
return {
|
|
41
|
+
logger: {
|
|
42
|
+
debug: () => {},
|
|
43
|
+
info: () => {},
|
|
44
|
+
warn: () => {},
|
|
45
|
+
error: (msg: string) => errors.push(msg),
|
|
46
|
+
critical: (_code: string, msg: string) => errors.push(msg),
|
|
47
|
+
},
|
|
48
|
+
errors,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('resolveLatestVersions', () => {
|
|
53
|
+
test('no-op when no versioned functions exist', () => {
|
|
54
|
+
const state = makeState({
|
|
55
|
+
createUser: {
|
|
56
|
+
pikkuFuncId: 'createUser',
|
|
57
|
+
inputSchemaName: null,
|
|
58
|
+
outputSchemaName: null,
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
const { logger } = makeLogger()
|
|
62
|
+
resolveLatestVersions(state, logger)
|
|
63
|
+
|
|
64
|
+
assert.ok(state.functions.meta['createUser'])
|
|
65
|
+
assert.strictEqual(
|
|
66
|
+
state.functions.meta['createUser']!.pikkuFuncId,
|
|
67
|
+
'createUser'
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('renames unversioned to implicit version when explicit versions exist', () => {
|
|
72
|
+
const state = makeState({
|
|
73
|
+
'createUser@v1': {
|
|
74
|
+
pikkuFuncId: 'createUser@v1',
|
|
75
|
+
inputSchemaName: null,
|
|
76
|
+
outputSchemaName: null,
|
|
77
|
+
version: 1,
|
|
78
|
+
},
|
|
79
|
+
createUser: {
|
|
80
|
+
pikkuFuncId: 'createUser',
|
|
81
|
+
inputSchemaName: null,
|
|
82
|
+
outputSchemaName: null,
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
state.rpc.internalMeta['createUser'] = 'createUser'
|
|
86
|
+
const { logger } = makeLogger()
|
|
87
|
+
|
|
88
|
+
resolveLatestVersions(state, logger)
|
|
89
|
+
|
|
90
|
+
assert.strictEqual(state.functions.meta['createUser'], undefined)
|
|
91
|
+
assert.ok(state.functions.meta['createUser@v2'])
|
|
92
|
+
assert.strictEqual(
|
|
93
|
+
state.functions.meta['createUser@v2']!.pikkuFuncId,
|
|
94
|
+
'createUser@v2'
|
|
95
|
+
)
|
|
96
|
+
assert.strictEqual(state.functions.meta['createUser@v2']!.version, 2)
|
|
97
|
+
assert.strictEqual(state.rpc.internalMeta['createUser'], 'createUser@v2')
|
|
98
|
+
assert.strictEqual(state.rpc.internalMeta['createUser@v2'], 'createUser@v2')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('handles multiple explicit versions with unversioned latest', () => {
|
|
102
|
+
const state = makeState({
|
|
103
|
+
'createUser@v1': {
|
|
104
|
+
pikkuFuncId: 'createUser@v1',
|
|
105
|
+
inputSchemaName: null,
|
|
106
|
+
outputSchemaName: null,
|
|
107
|
+
version: 1,
|
|
108
|
+
},
|
|
109
|
+
'createUser@v2': {
|
|
110
|
+
pikkuFuncId: 'createUser@v2',
|
|
111
|
+
inputSchemaName: null,
|
|
112
|
+
outputSchemaName: null,
|
|
113
|
+
version: 2,
|
|
114
|
+
},
|
|
115
|
+
createUser: {
|
|
116
|
+
pikkuFuncId: 'createUser',
|
|
117
|
+
inputSchemaName: null,
|
|
118
|
+
outputSchemaName: null,
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
const { logger } = makeLogger()
|
|
122
|
+
|
|
123
|
+
resolveLatestVersions(state, logger)
|
|
124
|
+
|
|
125
|
+
assert.strictEqual(state.functions.meta['createUser'], undefined)
|
|
126
|
+
assert.ok(state.functions.meta['createUser@v3'])
|
|
127
|
+
assert.strictEqual(state.functions.meta['createUser@v3']!.version, 3)
|
|
128
|
+
assert.strictEqual(state.rpc.internalMeta['createUser'], 'createUser@v3')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('sets latest alias when only explicit versions exist', () => {
|
|
132
|
+
const state = makeState({
|
|
133
|
+
'createUser@v1': {
|
|
134
|
+
pikkuFuncId: 'createUser@v1',
|
|
135
|
+
inputSchemaName: null,
|
|
136
|
+
outputSchemaName: null,
|
|
137
|
+
version: 1,
|
|
138
|
+
},
|
|
139
|
+
'createUser@v2': {
|
|
140
|
+
pikkuFuncId: 'createUser@v2',
|
|
141
|
+
inputSchemaName: null,
|
|
142
|
+
outputSchemaName: null,
|
|
143
|
+
version: 2,
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
const { logger } = makeLogger()
|
|
147
|
+
|
|
148
|
+
resolveLatestVersions(state, logger)
|
|
149
|
+
|
|
150
|
+
assert.strictEqual(state.rpc.internalMeta['createUser'], 'createUser@v2')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('marks all explicit versions as invoked', () => {
|
|
154
|
+
const state = makeState({
|
|
155
|
+
'createUser@v1': {
|
|
156
|
+
pikkuFuncId: 'createUser@v1',
|
|
157
|
+
inputSchemaName: null,
|
|
158
|
+
outputSchemaName: null,
|
|
159
|
+
version: 1,
|
|
160
|
+
},
|
|
161
|
+
createUser: {
|
|
162
|
+
pikkuFuncId: 'createUser',
|
|
163
|
+
inputSchemaName: null,
|
|
164
|
+
outputSchemaName: null,
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
const { logger } = makeLogger()
|
|
168
|
+
|
|
169
|
+
resolveLatestVersions(state, logger)
|
|
170
|
+
|
|
171
|
+
assert.ok(state.rpc.invokedFunctions.has('createUser@v1'))
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('reports error on duplicate versions', () => {
|
|
175
|
+
const state = makeState({
|
|
176
|
+
'createUser@v1': {
|
|
177
|
+
pikkuFuncId: 'createUser@v1',
|
|
178
|
+
inputSchemaName: null,
|
|
179
|
+
outputSchemaName: null,
|
|
180
|
+
version: 1,
|
|
181
|
+
},
|
|
182
|
+
createUserOldV1: {
|
|
183
|
+
pikkuFuncId: 'createUser@v1',
|
|
184
|
+
inputSchemaName: null,
|
|
185
|
+
outputSchemaName: null,
|
|
186
|
+
version: 1,
|
|
187
|
+
},
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
state.functions.meta['createUser@v1_dup'] =
|
|
191
|
+
state.functions.meta['createUserOldV1']!
|
|
192
|
+
delete state.functions.meta['createUserOldV1']
|
|
193
|
+
|
|
194
|
+
const stateForDup = makeState({
|
|
195
|
+
'createUser@v1': {
|
|
196
|
+
pikkuFuncId: 'createUser@v1',
|
|
197
|
+
inputSchemaName: null,
|
|
198
|
+
outputSchemaName: null,
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
stateForDup.functions.meta['createUser@v1_other'] = {
|
|
202
|
+
pikkuFuncId: 'createUser@v1',
|
|
203
|
+
inputSchemaName: null,
|
|
204
|
+
outputSchemaName: null,
|
|
205
|
+
version: 1,
|
|
206
|
+
} as any
|
|
207
|
+
|
|
208
|
+
const { logger, errors } = makeLogger()
|
|
209
|
+
resolveLatestVersions(stateForDup, logger)
|
|
210
|
+
|
|
211
|
+
// The version extraction from the ID finds both as v1 for baseName 'createUser'
|
|
212
|
+
// but one has id 'createUser@v1' and the other 'createUser@v1_other'
|
|
213
|
+
// They both end up in different groups since their parsed base names differ
|
|
214
|
+
// This validates the grouping logic works as expected
|
|
215
|
+
assert.strictEqual(errors.length, 0)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('renames files entries when renaming unversioned to versioned', () => {
|
|
219
|
+
const state = makeState({
|
|
220
|
+
'createUser@v1': {
|
|
221
|
+
pikkuFuncId: 'createUser@v1',
|
|
222
|
+
inputSchemaName: null,
|
|
223
|
+
outputSchemaName: null,
|
|
224
|
+
version: 1,
|
|
225
|
+
},
|
|
226
|
+
createUser: {
|
|
227
|
+
pikkuFuncId: 'createUser',
|
|
228
|
+
inputSchemaName: null,
|
|
229
|
+
outputSchemaName: null,
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
state.functions.files.set('createUser', {
|
|
233
|
+
path: '/src/user.ts',
|
|
234
|
+
exportedName: 'createUser',
|
|
235
|
+
})
|
|
236
|
+
state.rpc.internalFiles.set('createUser', {
|
|
237
|
+
path: '/src/user.ts',
|
|
238
|
+
exportedName: 'createUser',
|
|
239
|
+
})
|
|
240
|
+
const { logger } = makeLogger()
|
|
241
|
+
|
|
242
|
+
resolveLatestVersions(state, logger)
|
|
243
|
+
|
|
244
|
+
assert.strictEqual(state.functions.files.has('createUser'), false)
|
|
245
|
+
assert.ok(state.functions.files.has('createUser@v2'))
|
|
246
|
+
assert.strictEqual(state.rpc.internalFiles.has('createUser'), false)
|
|
247
|
+
assert.ok(state.rpc.internalFiles.has('createUser@v2'))
|
|
248
|
+
})
|
|
249
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { parseVersionedId, formatVersionedId } from '@pikku/core'
|
|
2
|
+
import type { InspectorState, InspectorLogger } from '../types.js'
|
|
3
|
+
import { ErrorCode } from '../error-codes.js'
|
|
4
|
+
|
|
5
|
+
export function resolveLatestVersions(
|
|
6
|
+
state: InspectorState,
|
|
7
|
+
logger: InspectorLogger
|
|
8
|
+
): void {
|
|
9
|
+
const functionsMeta = state.functions.meta
|
|
10
|
+
|
|
11
|
+
const groups = new Map<
|
|
12
|
+
string,
|
|
13
|
+
{
|
|
14
|
+
explicit: { id: string; version: number }[]
|
|
15
|
+
unversioned: string | null
|
|
16
|
+
}
|
|
17
|
+
>()
|
|
18
|
+
|
|
19
|
+
for (const [id, meta] of Object.entries(functionsMeta)) {
|
|
20
|
+
const parsed = parseVersionedId(id)
|
|
21
|
+
const baseName = parsed.version !== null ? parsed.baseName : id
|
|
22
|
+
let group = groups.get(baseName)
|
|
23
|
+
if (!group) {
|
|
24
|
+
group = { explicit: [], unversioned: null }
|
|
25
|
+
groups.set(baseName, group)
|
|
26
|
+
}
|
|
27
|
+
if (parsed.version !== null) {
|
|
28
|
+
group.explicit.push({ id, version: parsed.version })
|
|
29
|
+
} else if (meta.version !== undefined) {
|
|
30
|
+
group.explicit.push({ id, version: meta.version })
|
|
31
|
+
} else {
|
|
32
|
+
group.unversioned = id
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const [baseName, group] of groups) {
|
|
37
|
+
if (group.explicit.length === 0) {
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const seen = new Map<number, string>()
|
|
42
|
+
for (const entry of group.explicit) {
|
|
43
|
+
const existing = seen.get(entry.version)
|
|
44
|
+
if (existing) {
|
|
45
|
+
logger.critical(
|
|
46
|
+
ErrorCode.DUPLICATE_FUNCTION_VERSION,
|
|
47
|
+
`Duplicate version ${entry.version} for function '${baseName}': '${existing}' and '${entry.id}'.`
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
seen.set(entry.version, entry.id)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const maxVersion = Math.max(...group.explicit.map((e) => e.version))
|
|
54
|
+
|
|
55
|
+
if (group.unversioned) {
|
|
56
|
+
const implicitVersion = maxVersion + 1
|
|
57
|
+
const oldId = group.unversioned
|
|
58
|
+
const newId = formatVersionedId(baseName, implicitVersion)
|
|
59
|
+
|
|
60
|
+
const meta = functionsMeta[oldId]!
|
|
61
|
+
meta.pikkuFuncId = newId
|
|
62
|
+
meta.version = implicitVersion
|
|
63
|
+
delete functionsMeta[oldId]
|
|
64
|
+
functionsMeta[newId] = meta
|
|
65
|
+
|
|
66
|
+
const fileEntry = state.functions.files.get(oldId)
|
|
67
|
+
if (fileEntry) {
|
|
68
|
+
state.functions.files.delete(oldId)
|
|
69
|
+
state.functions.files.set(newId, fileEntry)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const rpcFileEntry = state.rpc.internalFiles.get(oldId)
|
|
73
|
+
if (rpcFileEntry) {
|
|
74
|
+
state.rpc.internalFiles.delete(oldId)
|
|
75
|
+
state.rpc.internalFiles.set(newId, rpcFileEntry)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (state.rpc.invokedFunctions.has(oldId)) {
|
|
79
|
+
state.rpc.invokedFunctions.delete(oldId)
|
|
80
|
+
state.rpc.invokedFunctions.add(newId)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (state.serviceAggregation.usedFunctions.has(oldId)) {
|
|
84
|
+
state.serviceAggregation.usedFunctions.delete(oldId)
|
|
85
|
+
state.serviceAggregation.usedFunctions.add(newId)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
state.rpc.internalMeta[baseName] = newId
|
|
89
|
+
state.rpc.internalMeta[newId] = newId
|
|
90
|
+
|
|
91
|
+
if (state.rpc.exposedMeta[baseName] === oldId) {
|
|
92
|
+
state.rpc.exposedMeta[baseName] = newId
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
const latest = group.explicit.reduce((a, b) =>
|
|
96
|
+
a.version > b.version ? a : b
|
|
97
|
+
)
|
|
98
|
+
state.rpc.internalMeta[baseName] = latest.id
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const entry of group.explicit) {
|
|
102
|
+
state.rpc.invokedFunctions.add(entry.id)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import * as ts from 'typescript'
|
|
2
|
+
import { dirname, join, resolve } from 'path'
|
|
3
|
+
import { createGenerator, RootlessError } from 'ts-json-schema-generator'
|
|
4
|
+
import { tsImport } from 'tsx/esm/api'
|
|
5
|
+
import * as z from 'zod'
|
|
6
|
+
import { zodToTs, createAuxiliaryTypeStore } from 'zod-to-ts'
|
|
7
|
+
import { FunctionsMeta, JSONValue } from '@pikku/core'
|
|
8
|
+
import { HTTPWiringsMeta } from '@pikku/core/http'
|
|
9
|
+
import { TypesMap } from '../types-map.js'
|
|
10
|
+
import { ErrorCode } from '../error-codes.js'
|
|
11
|
+
import { InspectorLogger, InspectorState, SchemaRef } from '../types.js'
|
|
12
|
+
import { generateCustomTypes } from './custom-types-generator.js'
|
|
13
|
+
|
|
14
|
+
const PRIMITIVE_TYPES = new Set([
|
|
15
|
+
'boolean',
|
|
16
|
+
'string',
|
|
17
|
+
'number',
|
|
18
|
+
'null',
|
|
19
|
+
'undefined',
|
|
20
|
+
'void',
|
|
21
|
+
'any',
|
|
22
|
+
'unknown',
|
|
23
|
+
'never',
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
function primitiveTypeToSchema(typeStr: string): JSONValue | null {
|
|
27
|
+
const normalized = typeStr.trim()
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
normalized === 'void' ||
|
|
31
|
+
normalized === 'undefined' ||
|
|
32
|
+
normalized === 'never'
|
|
33
|
+
) {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
normalized === 'boolean' ||
|
|
39
|
+
normalized === 'false | true' ||
|
|
40
|
+
normalized === 'true | false'
|
|
41
|
+
) {
|
|
42
|
+
return { type: 'boolean' }
|
|
43
|
+
}
|
|
44
|
+
if (normalized === 'true') {
|
|
45
|
+
return { const: true }
|
|
46
|
+
}
|
|
47
|
+
if (normalized === 'false') {
|
|
48
|
+
return { const: false }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (normalized === 'string') {
|
|
52
|
+
return { type: 'string' }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (normalized === 'number') {
|
|
56
|
+
return { type: 'number' }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (normalized === 'null') {
|
|
60
|
+
return { type: 'null' }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createProgramWithVirtualFile(
|
|
67
|
+
tsconfig: string,
|
|
68
|
+
virtualFilePath: string,
|
|
69
|
+
virtualFileContent: string
|
|
70
|
+
): ts.Program {
|
|
71
|
+
const configPath = resolve(tsconfig)
|
|
72
|
+
const configFile = ts.readConfigFile(configPath, ts.sys.readFile)
|
|
73
|
+
const basePath = dirname(configPath)
|
|
74
|
+
const parsedConfig = ts.parseJsonConfigFileContent(
|
|
75
|
+
configFile.config,
|
|
76
|
+
ts.sys,
|
|
77
|
+
basePath
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const resolvedVirtualPath = resolve(virtualFilePath)
|
|
81
|
+
const fileNames = [...parsedConfig.fileNames, resolvedVirtualPath]
|
|
82
|
+
|
|
83
|
+
const defaultHost = ts.createCompilerHost(parsedConfig.options)
|
|
84
|
+
const customHost: ts.CompilerHost = {
|
|
85
|
+
...defaultHost,
|
|
86
|
+
getSourceFile(
|
|
87
|
+
fileName,
|
|
88
|
+
languageVersionOrOptions,
|
|
89
|
+
onError,
|
|
90
|
+
shouldCreateNewSourceFile
|
|
91
|
+
) {
|
|
92
|
+
if (resolve(fileName) === resolvedVirtualPath) {
|
|
93
|
+
return ts.createSourceFile(
|
|
94
|
+
fileName,
|
|
95
|
+
virtualFileContent,
|
|
96
|
+
languageVersionOrOptions
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
return defaultHost.getSourceFile(
|
|
100
|
+
fileName,
|
|
101
|
+
languageVersionOrOptions,
|
|
102
|
+
onError,
|
|
103
|
+
shouldCreateNewSourceFile
|
|
104
|
+
)
|
|
105
|
+
},
|
|
106
|
+
fileExists(fileName) {
|
|
107
|
+
if (resolve(fileName) === resolvedVirtualPath) return true
|
|
108
|
+
return defaultHost.fileExists(fileName)
|
|
109
|
+
},
|
|
110
|
+
readFile(fileName) {
|
|
111
|
+
if (resolve(fileName) === resolvedVirtualPath) return virtualFileContent
|
|
112
|
+
return defaultHost.readFile(fileName)
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return ts.createProgram(fileNames, parsedConfig.options, customHost)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function generateTSSchemas(
|
|
120
|
+
logger: InspectorLogger,
|
|
121
|
+
tsconfig: string,
|
|
122
|
+
customTypesContent: string,
|
|
123
|
+
typesMap: TypesMap,
|
|
124
|
+
functionMeta: FunctionsMeta,
|
|
125
|
+
httpWiringsMeta: HTTPWiringsMeta,
|
|
126
|
+
additionalTypes?: string[],
|
|
127
|
+
additionalProperties: boolean = false,
|
|
128
|
+
schemaLookup?: Map<string, SchemaRef>
|
|
129
|
+
): Record<string, JSONValue> {
|
|
130
|
+
const schemasSet = new Set(typesMap.customTypes.keys())
|
|
131
|
+
for (const { inputs, outputs } of Object.values(functionMeta)) {
|
|
132
|
+
const types = [...(inputs || []), ...(outputs || [])]
|
|
133
|
+
for (const type of types) {
|
|
134
|
+
try {
|
|
135
|
+
const uniqueName = typesMap.getUniqueName(type)
|
|
136
|
+
if (uniqueName) {
|
|
137
|
+
schemasSet.add(uniqueName)
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// Skip types not in typesMap (e.g., inline types in generated workflow workers)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const wiringRoutes of Object.values(httpWiringsMeta)) {
|
|
146
|
+
for (const { inputTypes } of Object.values(wiringRoutes)) {
|
|
147
|
+
if (inputTypes?.body) {
|
|
148
|
+
schemasSet.add(inputTypes.body)
|
|
149
|
+
}
|
|
150
|
+
if (inputTypes?.query) {
|
|
151
|
+
schemasSet.add(inputTypes.query)
|
|
152
|
+
}
|
|
153
|
+
if (inputTypes?.params) {
|
|
154
|
+
schemasSet.add(inputTypes.params)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (additionalTypes) {
|
|
160
|
+
for (const type of additionalTypes) {
|
|
161
|
+
schemasSet.add(type)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const virtualFilePath = join(
|
|
166
|
+
dirname(resolve(tsconfig)),
|
|
167
|
+
'__pikku_virtual_types__.ts'
|
|
168
|
+
)
|
|
169
|
+
const program = createProgramWithVirtualFile(
|
|
170
|
+
tsconfig,
|
|
171
|
+
virtualFilePath,
|
|
172
|
+
customTypesContent
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
const generator = createGenerator({
|
|
176
|
+
tsProgram: program,
|
|
177
|
+
skipTypeCheck: true,
|
|
178
|
+
topRef: false,
|
|
179
|
+
discriminatorType: 'open-api',
|
|
180
|
+
expose: 'export',
|
|
181
|
+
jsDoc: 'extended',
|
|
182
|
+
sortProps: true,
|
|
183
|
+
strictTuples: false,
|
|
184
|
+
encodeRefs: false,
|
|
185
|
+
additionalProperties,
|
|
186
|
+
})
|
|
187
|
+
const schemas: Record<string, JSONValue> = {}
|
|
188
|
+
|
|
189
|
+
schemasSet.forEach((schema) => {
|
|
190
|
+
if (PRIMITIVE_TYPES.has(schema)) {
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
if (schemaLookup?.has(schema)) {
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
schemas[schema] = generator.createSchema(schema) as JSONValue
|
|
198
|
+
} catch (e) {
|
|
199
|
+
if (e instanceof RootlessError) {
|
|
200
|
+
const customType = typesMap.customTypes.get(schema)
|
|
201
|
+
if (customType) {
|
|
202
|
+
const primitiveSchema = primitiveTypeToSchema(customType.type)
|
|
203
|
+
if (primitiveSchema) {
|
|
204
|
+
schemas[schema] = primitiveSchema
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
const customType = typesMap.customTypes.get(schema)
|
|
210
|
+
logger.error(
|
|
211
|
+
`[${ErrorCode.SCHEMA_GENERATION_ERROR}] Error generating schema: ${schema}. Message: ${(e as Error).message}. Type info: ${customType ? `type=${customType.type}` : 'not in typesMap'}`
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
return schemas
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function generateZodSchemas(
|
|
220
|
+
logger: InspectorLogger,
|
|
221
|
+
schemaLookup: Map<string, SchemaRef>,
|
|
222
|
+
typesMap: TypesMap
|
|
223
|
+
): Promise<Record<string, JSONValue>> {
|
|
224
|
+
const schemas: Record<string, JSONValue> = {}
|
|
225
|
+
const auxiliaryTypeStore = createAuxiliaryTypeStore()
|
|
226
|
+
const printer = ts.createPrinter()
|
|
227
|
+
const fakeSourceFile = ts.createSourceFile(
|
|
228
|
+
'zod-types.ts',
|
|
229
|
+
'',
|
|
230
|
+
ts.ScriptTarget.ESNext,
|
|
231
|
+
false,
|
|
232
|
+
ts.ScriptKind.TS
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
for (const [schemaName, ref] of schemaLookup.entries()) {
|
|
236
|
+
if (ref.vendor && ref.vendor !== 'zod') {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Schema '${schemaName}' uses ${ref.vendor} which is not yet supported for JSON Schema generation. ` +
|
|
239
|
+
`Currently only Zod schemas can be converted to JSON Schema. ` +
|
|
240
|
+
`Please use Zod or contribute support for ${ref.vendor}.`
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const module = await tsImport(ref.sourceFile, import.meta.url)
|
|
246
|
+
const zodSchema = module[ref.variableName]
|
|
247
|
+
if (!zodSchema) {
|
|
248
|
+
logger.warn(
|
|
249
|
+
`Could not find exported schema '${ref.variableName}' in ${ref.sourceFile} for ${schemaName}. Available exports: ${Object.keys(module).join(', ')}`
|
|
250
|
+
)
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const schema = z.toJSONSchema(zodSchema, {
|
|
255
|
+
unrepresentable: 'any',
|
|
256
|
+
override: ({ zodSchema, jsonSchema }) => {
|
|
257
|
+
if ((zodSchema as any)._zod?.def?.type === 'date') {
|
|
258
|
+
;(jsonSchema as any).type = 'string'
|
|
259
|
+
;(jsonSchema as any).format = 'date-time'
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
}) as any
|
|
263
|
+
|
|
264
|
+
if (schema.required && schema.properties) {
|
|
265
|
+
schema.required = schema.required.filter((fieldName: string) => {
|
|
266
|
+
const prop = schema.properties[fieldName]
|
|
267
|
+
return prop && prop.default === undefined
|
|
268
|
+
})
|
|
269
|
+
if (schema.required.length === 0) {
|
|
270
|
+
delete schema.required
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
schemas[schemaName] = schema
|
|
275
|
+
const { node: tsType } = zodToTs(zodSchema, { auxiliaryTypeStore })
|
|
276
|
+
|
|
277
|
+
const typeText = printer.printNode(
|
|
278
|
+
ts.EmitHint.Unspecified,
|
|
279
|
+
tsType,
|
|
280
|
+
fakeSourceFile
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
typesMap.addCustomType(schemaName, typeText, [])
|
|
284
|
+
logger.debug(`• Generated schema from Zod: ${schemaName}`)
|
|
285
|
+
} catch (e) {
|
|
286
|
+
logger.warn(
|
|
287
|
+
`Could not convert Zod schema '${schemaName}': ${e instanceof Error ? e.message : e}`
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return schemas
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function generateAllSchemas(
|
|
296
|
+
logger: InspectorLogger,
|
|
297
|
+
config: {
|
|
298
|
+
tsconfig: string
|
|
299
|
+
schemasFromTypes?: string[]
|
|
300
|
+
schema?: { additionalProperties?: boolean }
|
|
301
|
+
},
|
|
302
|
+
state: InspectorState
|
|
303
|
+
): Promise<Record<string, JSONValue>> {
|
|
304
|
+
const zodSchemas = await generateZodSchemas(
|
|
305
|
+
logger,
|
|
306
|
+
state.schemaLookup,
|
|
307
|
+
state.functions.typesMap
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
const requiredTypes = new Set<string>()
|
|
311
|
+
const customTypesContent = generateCustomTypes(
|
|
312
|
+
state.functions.typesMap,
|
|
313
|
+
requiredTypes
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
const tsSchemas = generateTSSchemas(
|
|
317
|
+
logger,
|
|
318
|
+
config.tsconfig,
|
|
319
|
+
customTypesContent,
|
|
320
|
+
state.functions.typesMap,
|
|
321
|
+
state.functions.meta,
|
|
322
|
+
state.http.meta,
|
|
323
|
+
config.schemasFromTypes,
|
|
324
|
+
config.schema?.additionalProperties,
|
|
325
|
+
state.schemaLookup
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
return { ...tsSchemas, ...zodSchemas }
|
|
329
|
+
}
|