@teleporthq/teleport-plugin-next-workflows 0.43.35 → 0.43.37
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/__tests__/ai-error-contract.test.ts +149 -0
- package/__tests__/data-api-safe-query.test.ts +16 -16
- package/__tests__/node-handler-file-inline-map.test.ts +114 -0
- package/__tests__/raw-query-param-binding.test.ts +18 -17
- package/__tests__/streaming-on-end-env-filter.test.ts +405 -0
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +14 -8
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
- package/dist/cjs/data-api-route-generator.js +13 -2
- package/dist/cjs/data-api-route-generator.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +47 -5
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/nodes/general/general-custom-js.d.ts.map +1 -1
- package/dist/cjs/nodes/general/general-custom-js.js +7 -0
- package/dist/cjs/nodes/general/general-custom-js.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/workflow-component-plugin.js +9 -3
- package/dist/cjs/workflow-component-plugin.js.map +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts +1 -0
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +83 -5
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +14 -8
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/data-api-route-generator.d.ts.map +1 -1
- package/dist/esm/data-api-route-generator.js +13 -2
- package/dist/esm/data-api-route-generator.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +47 -5
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/nodes/general/general-custom-js.d.ts.map +1 -1
- package/dist/esm/nodes/general/general-custom-js.js +7 -0
- package/dist/esm/nodes/general/general-custom-js.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/workflow-component-plugin.js +9 -3
- package/dist/esm/workflow-component-plugin.js.map +1 -1
- package/dist/esm/workflow-project-plugin.d.ts +1 -0
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +83 -5
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/package.json +2 -2
- package/src/api-route-generator.ts +14 -8
- package/src/data-api-route-generator.ts +13 -2
- package/src/executor-generator.ts +47 -5
- package/src/nodes/general/general-custom-js.ts +8 -0
- package/src/workflow-component-plugin.ts +15 -10
- package/src/workflow-project-plugin.ts +87 -4
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Regression guard for "AI provider/auth failures treated as success".
|
|
2
|
+
//
|
|
3
|
+
// Every AI node handler (src/nodes/ai/*) signals failure with
|
|
4
|
+
// `{ error: true, message, code }` — a BOOLEAN error flag — while the executor
|
|
5
|
+
// error gates only tested `typeof result.error === 'string'`. A missing
|
|
6
|
+
// OPENAI key therefore sailed through as a successful node result and the
|
|
7
|
+
// pipeline limped into a downstream NOT NULL 500 (e.g. persisting a NULL chat
|
|
8
|
+
// answer). The gates now treat `result.error === true` as fatal too, via the
|
|
9
|
+
// shared isFatalNodeResult / fatalNodeResultMessage runtime helpers.
|
|
10
|
+
|
|
11
|
+
import { generateSharedRuntimeUtilsCode } from '../src/executor-generator'
|
|
12
|
+
import {
|
|
13
|
+
generateServerSegmentAPIRoute,
|
|
14
|
+
generateStreamingServerSegmentAPIRoute,
|
|
15
|
+
} from '../src/api-route-generator'
|
|
16
|
+
import type { WorkflowSegment } from '../src/types'
|
|
17
|
+
|
|
18
|
+
type UtilsModule = {
|
|
19
|
+
isFatalNodeResult: (result: unknown) => boolean
|
|
20
|
+
fatalNodeResultMessage: (result: Record<string, unknown>) => string
|
|
21
|
+
executeNodes: (
|
|
22
|
+
nodes: unknown[],
|
|
23
|
+
edges: unknown[],
|
|
24
|
+
context: Record<string, unknown>,
|
|
25
|
+
nodeHandlers: Record<string, unknown>,
|
|
26
|
+
workflowConfig: unknown,
|
|
27
|
+
callServerSegment: unknown,
|
|
28
|
+
executionId: string
|
|
29
|
+
) => Promise<void>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function loadUtils(): UtilsModule {
|
|
33
|
+
const utilsModule = { exports: {} as unknown as UtilsModule }
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
35
|
+
new Function('module', 'exports', 'require', generateSharedRuntimeUtilsCode())(
|
|
36
|
+
utilsModule,
|
|
37
|
+
utilsModule.exports,
|
|
38
|
+
() => ({})
|
|
39
|
+
)
|
|
40
|
+
return utilsModule.exports
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('isFatalNodeResult — error contract normalization', () => {
|
|
44
|
+
const utils = loadUtils()
|
|
45
|
+
|
|
46
|
+
it('treats the AI-node contract { error: true, message, code } as fatal', () => {
|
|
47
|
+
expect(
|
|
48
|
+
utils.isFatalNodeResult({ error: true, message: 'No API key', code: 'authentication_error' })
|
|
49
|
+
).toBeTruthy()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('keeps the legacy string-error contract fatal', () => {
|
|
53
|
+
expect(utils.isFatalNodeResult({ error: 'boom' })).toBeTruthy()
|
|
54
|
+
expect(utils.isFatalNodeResult({ success: false })).toBeTruthy()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('does not flag successful results', () => {
|
|
58
|
+
expect(utils.isFatalNodeResult({ result: 'ok' })).toBeFalsy()
|
|
59
|
+
expect(utils.isFatalNodeResult({ error: false })).toBeFalsy()
|
|
60
|
+
expect(utils.isFatalNodeResult({ error: '' })).toBeFalsy()
|
|
61
|
+
expect(utils.isFatalNodeResult(null)).toBeFalsy()
|
|
62
|
+
expect(utils.isFatalNodeResult(undefined)).toBeFalsy()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('surfaces the AI-node message when error is boolean', () => {
|
|
66
|
+
expect(
|
|
67
|
+
utils.fatalNodeResultMessage({ error: true, message: 'Incorrect API key provided' })
|
|
68
|
+
).toBe('Incorrect API key provided')
|
|
69
|
+
expect(utils.fatalNodeResultMessage({ error: 'raw string error' })).toBe('raw string error')
|
|
70
|
+
expect(utils.fatalNodeResultMessage({ success: false })).toBe('Node execution failed')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('executeNodes halts on { error: true } and never reaches downstream nodes', () => {
|
|
75
|
+
const utils = loadUtils()
|
|
76
|
+
|
|
77
|
+
const nodes = [
|
|
78
|
+
{ id: 'ai-1', type: 'ai-custom-prompt', config: {}, stepNumber: 1 },
|
|
79
|
+
{ id: 'persist-1', type: 'data-create-item', config: {}, stepNumber: 2 },
|
|
80
|
+
]
|
|
81
|
+
const edges = [{ id: 'e1', source: 'ai-1', target: 'persist-1' }]
|
|
82
|
+
|
|
83
|
+
it('throws with the provider message and skips the downstream persist node', async () => {
|
|
84
|
+
const persistCalls: unknown[] = []
|
|
85
|
+
const handlers = {
|
|
86
|
+
'ai-custom-prompt': async () => ({
|
|
87
|
+
error: true,
|
|
88
|
+
message: 'AI provider authentication failed',
|
|
89
|
+
code: 'authentication_error',
|
|
90
|
+
}),
|
|
91
|
+
'data-create-item': async () => {
|
|
92
|
+
persistCalls.push('persist')
|
|
93
|
+
return { success: true }
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await expect(
|
|
98
|
+
utils.executeNodes(nodes, edges, {}, handlers, { nodes, edges }, null, 'exec-1')
|
|
99
|
+
).rejects.toThrow('AI provider authentication failed')
|
|
100
|
+
expect(persistCalls).toHaveLength(0)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('still runs downstream nodes for successful AI results', async () => {
|
|
104
|
+
const persistCalls: unknown[] = []
|
|
105
|
+
const handlers = {
|
|
106
|
+
'ai-custom-prompt': async () => ({ response: 'hello', model: 'gpt-4o' }),
|
|
107
|
+
'data-create-item': async () => {
|
|
108
|
+
persistCalls.push('persist')
|
|
109
|
+
return { success: true }
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await utils.executeNodes(nodes, edges, {}, handlers, { nodes, edges }, null, 'exec-2')
|
|
114
|
+
expect(persistCalls).toHaveLength(1)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('generated server routes gate node results through the shared helpers', () => {
|
|
119
|
+
const segment: WorkflowSegment = {
|
|
120
|
+
id: 'server-1',
|
|
121
|
+
env: 'server',
|
|
122
|
+
nodeIds: ['ai-1'],
|
|
123
|
+
nodes: [
|
|
124
|
+
{
|
|
125
|
+
id: 'ai-1',
|
|
126
|
+
type: 'ai-custom-prompt',
|
|
127
|
+
config: { prompt: 'hi', streaming: true },
|
|
128
|
+
stepNumber: 1,
|
|
129
|
+
} as never,
|
|
130
|
+
],
|
|
131
|
+
edges: [],
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
it('non-streaming segment route uses isFatalNodeResult', () => {
|
|
135
|
+
const route = generateServerSegmentAPIRoute(
|
|
136
|
+
{ ...segment, nodes: [{ ...segment.nodes[0], config: { prompt: 'hi' } } as never] },
|
|
137
|
+
'Chat'
|
|
138
|
+
)
|
|
139
|
+
expect(route).toContain('utils.isFatalNodeResult(result)')
|
|
140
|
+
expect(route).toContain('utils.fatalNodeResultMessage(result)')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('streaming segment route gates the streaming AI result itself before running on-end nodes', () => {
|
|
144
|
+
const route = generateStreamingServerSegmentAPIRoute(segment, 'Chat')
|
|
145
|
+
// Two gates: the streaming AI node's own result + the non-streaming path.
|
|
146
|
+
const occurrences = route.split('utils.isFatalNodeResult(result)').length - 1
|
|
147
|
+
expect(occurrences).toBeGreaterThanOrEqual(2)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
@@ -98,30 +98,30 @@ describe('data-api safeQuery wrapper — defends against Postgres type-coercion
|
|
|
98
98
|
|
|
99
99
|
describe('data-api safeQuery — runtime semantics', () => {
|
|
100
100
|
const code = generateDataAPIRoute()
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const evalFn = () => {
|
|
104
|
-
const startTag = 'async function safeQuery'
|
|
105
|
-
const start = code.indexOf(startTag)
|
|
106
|
-
// Find the matching closing brace via depth counting
|
|
101
|
+
const extractFunctionBlock = (src: string, tag: string): string => {
|
|
102
|
+
const start = src.indexOf(tag)
|
|
107
103
|
let depth = 0
|
|
108
|
-
let i =
|
|
109
|
-
for (; i <
|
|
110
|
-
if (
|
|
104
|
+
let i = src.indexOf('{', start)
|
|
105
|
+
for (; i < src.length; i++) {
|
|
106
|
+
if (src[i] === '{') {
|
|
111
107
|
depth++
|
|
112
|
-
} else if (
|
|
108
|
+
} else if (src[i] === '}') {
|
|
113
109
|
depth--
|
|
114
110
|
if (depth === 0) {
|
|
115
111
|
break
|
|
116
112
|
}
|
|
117
113
|
}
|
|
118
114
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
return src.slice(start, i + 1)
|
|
116
|
+
}
|
|
117
|
+
// Eval the safeQuery helper out of the emitted file and verify it
|
|
118
|
+
// behaves as we expect against a fake client.
|
|
119
|
+
const evalFn = () => {
|
|
120
|
+
const blockStart = code.indexOf('var SAFE_COERCION_ERROR_CODES')
|
|
121
|
+
const safeQueryStart = code.indexOf('async function safeQuery')
|
|
122
|
+
const preamble = code.slice(blockStart, safeQueryStart)
|
|
123
|
+
const safeQueryFn = extractFunctionBlock(code, 'async function safeQuery')
|
|
124
|
+
return new Function(preamble + safeQueryFn + '\nreturn safeQuery;')() as any
|
|
125
125
|
}
|
|
126
126
|
const safeQuery = evalFn()
|
|
127
127
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { NextWorkflowProjectPlugin } from '../src/workflow-project-plugin'
|
|
2
|
+
import { nodeRegistry } from '../src/nodes'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Regression (Vercel "ReferenceError: state_update_local_state is not defined" at
|
|
6
|
+
* "Collecting page data"): node-handlers-client.js / node-handlers-server.js used
|
|
7
|
+
* to emit separate top-level `async function <name>` declarations plus a
|
|
8
|
+
* `module.exports` map that referenced them by name. That forward-reference shape
|
|
9
|
+
* can dangle in production — a bundler/minifier that drops a single-use
|
|
10
|
+
* declaration, or a handler whose emitted name drifts from its map key, leaves the
|
|
11
|
+
* top-level map referencing an undefined identifier, which throws at MODULE-EVAL.
|
|
12
|
+
* The emitter now inlines each handler as its map value (a named function
|
|
13
|
+
* expression), so a dangling reference is structurally impossible.
|
|
14
|
+
*/
|
|
15
|
+
describe('generateNodeHandlerFile — inline handler map (dangling-proof)', () => {
|
|
16
|
+
const plugin = new NextWorkflowProjectPlugin()
|
|
17
|
+
const code: string = (plugin as any).generateNodeHandlerFile(
|
|
18
|
+
new Set(['state-update-local-state', 'toast-show', 'general-extract-form-data']),
|
|
19
|
+
'client'
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
it('inlines each handler inside a self-invoking function map value (no bare forward reference)', () => {
|
|
23
|
+
// The value is an IIFE that runs the handler (possibly multi-statement) and
|
|
24
|
+
// returns its entry function — never a bare top-level declaration reference.
|
|
25
|
+
expect(code).toMatch(/'state-update-local-state':\s*\(function \(\) \{/)
|
|
26
|
+
expect(code).toMatch(/return state_update_local_state;\s*\}\)\(\)/)
|
|
27
|
+
// The old fragile shape — a bare `'x-y': x_y` identifier reference — must be gone.
|
|
28
|
+
expect(code).not.toMatch(/'state-update-local-state':\s*state_update_local_state\s*[,}]/)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('has no dangling map reference (every value is defined at its own site)', () => {
|
|
32
|
+
const entry = /['"]([a-z0-9]+(?:-[a-z0-9]+)+)['"]\s*:\s*([a-z0-9]+(?:_[a-z0-9]+)+)\b/g
|
|
33
|
+
const dangling: string[] = []
|
|
34
|
+
let m: RegExpExecArray | null = entry.exec(code)
|
|
35
|
+
while (m !== null) {
|
|
36
|
+
const [, key, ident] = m
|
|
37
|
+
if (key.replace(/-/g, '_') === ident && !new RegExp(`function ${ident}\\b`).test(code)) {
|
|
38
|
+
dangling.push(ident)
|
|
39
|
+
}
|
|
40
|
+
m = entry.exec(code)
|
|
41
|
+
}
|
|
42
|
+
expect(dangling).toEqual([])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('evaluates as a CommonJS module without ReferenceError and exposes callable handlers', () => {
|
|
46
|
+
const moduleObj = { exports: {} as Record<string, unknown> }
|
|
47
|
+
// Simulate Next.js module-eval (the "Collecting page data" require). If a map
|
|
48
|
+
// value referenced an undefined identifier this would throw ReferenceError.
|
|
49
|
+
// eslint-disable-next-line no-new-func
|
|
50
|
+
new Function('module', 'exports', code)(moduleObj, moduleObj.exports)
|
|
51
|
+
expect(typeof moduleObj.exports['state-update-local-state']).toBe('function')
|
|
52
|
+
expect(typeof moduleObj.exports['toast-show']).toBe('function')
|
|
53
|
+
expect(typeof moduleObj.exports['general-extract-form-data']).toBe('function')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whole-registry contract: EVERY node type must produce a syntactically valid,
|
|
59
|
+
* module-eval-safe handler file in BOTH environments. The narrow 3-handler test
|
|
60
|
+
* above only exercised single-function CLIENT handlers, so it missed the class
|
|
61
|
+
* of handlers that emit MULTIPLE top-level statements — AI nodes
|
|
62
|
+
* (`generateAIProviderUtils() + '\n\n' + <fn>`), `payment-charge-user` and
|
|
63
|
+
* `general-rate-limiter` (function + appended helper declarations). Inlining
|
|
64
|
+
* those as a bare object-literal value produced `'ai-custom-prompt': var … `,
|
|
65
|
+
* a SyntaxError that crashed `require()` of node-handlers-server.js. This test
|
|
66
|
+
* evaluates the generated file for every registry type in both envs — the exact
|
|
67
|
+
* loop that surfaced the regression — so any future multi-statement or
|
|
68
|
+
* name-drifting handler fails CI here instead of at a Vercel build.
|
|
69
|
+
*/
|
|
70
|
+
describe('generateNodeHandlerFile — every registry node type is eval-safe in both envs', () => {
|
|
71
|
+
const plugin = new NextWorkflowProjectPlugin()
|
|
72
|
+
const allTypes = Object.keys(nodeRegistry)
|
|
73
|
+
|
|
74
|
+
for (const env of ['client', 'server'] as const) {
|
|
75
|
+
it(`compiles + evaluates a file containing every node type (${env})`, () => {
|
|
76
|
+
const code: string = (plugin as any).generateNodeHandlerFile(new Set(allTypes), env)
|
|
77
|
+
// Empty is legal only if no handler is relevant to this env — never here.
|
|
78
|
+
expect(code.length).toBeGreaterThan(0)
|
|
79
|
+
|
|
80
|
+
const moduleObj = { exports: {} as Record<string, unknown> }
|
|
81
|
+
expect(() => {
|
|
82
|
+
// eslint-disable-next-line no-new-func
|
|
83
|
+
new Function('module', 'exports', code)(moduleObj, moduleObj.exports)
|
|
84
|
+
}).not.toThrow()
|
|
85
|
+
|
|
86
|
+
// Each emitted map value must resolve to a callable handler (the IIFE
|
|
87
|
+
// returned the entry function, not `undefined` from a name drift).
|
|
88
|
+
for (const key of Object.keys(moduleObj.exports)) {
|
|
89
|
+
expect(typeof moduleObj.exports[key]).toBe('function')
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it(`emits an eval-safe single-type file for each node type individually (${env})`, () => {
|
|
94
|
+
const offenders: string[] = []
|
|
95
|
+
for (const type of allTypes) {
|
|
96
|
+
const code: string = (plugin as any).generateNodeHandlerFile(new Set([type]), env)
|
|
97
|
+
if (!code) {
|
|
98
|
+
continue // handler not relevant to this env — nothing emitted
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const moduleObj = { exports: {} as Record<string, unknown> }
|
|
102
|
+
// eslint-disable-next-line no-new-func
|
|
103
|
+
new Function('module', 'exports', code)(moduleObj, moduleObj.exports)
|
|
104
|
+
if (typeof moduleObj.exports[type] !== 'function') {
|
|
105
|
+
offenders.push(`${type} (value not callable)`)
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
offenders.push(`${type} (${(err as Error).message})`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
expect(offenders).toEqual([])
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
})
|
|
@@ -144,27 +144,28 @@ describe('raw-query param binding — generated data-API route binds, never inte
|
|
|
144
144
|
})
|
|
145
145
|
|
|
146
146
|
it('handleRawQuery actually passes params to a parameterized client.query at runtime', async () => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
147
|
+
const extractFunctionBlock = (src: string, tag: string): string => {
|
|
148
|
+
const start = src.indexOf(tag)
|
|
149
|
+
let depth = 0
|
|
150
|
+
let i = src.indexOf('{', start)
|
|
151
|
+
for (; i < src.length; i++) {
|
|
152
|
+
if (src[i] === '{') {
|
|
153
|
+
depth++
|
|
154
|
+
} else if (src[i] === '}') {
|
|
155
|
+
depth--
|
|
156
|
+
if (depth === 0) {
|
|
157
|
+
break
|
|
158
|
+
}
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
|
+
return src.slice(start, i + 1)
|
|
161
162
|
}
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
const
|
|
163
|
+
const blockStart = code.indexOf('var SAFE_COERCION_ERROR_CODES')
|
|
164
|
+
const safeQueryStart = code.indexOf('async function safeQuery')
|
|
165
|
+
const preamble = code.slice(blockStart, safeQueryStart)
|
|
166
|
+
const safeQueryFn = extractFunctionBlock(code, 'async function safeQuery')
|
|
166
167
|
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
167
|
-
const safeQuery = new Function(
|
|
168
|
+
const safeQuery = new Function(preamble + safeQueryFn + '\nreturn safeQuery;')() as any
|
|
168
169
|
|
|
169
170
|
const calls: Array<{ sql: string; params: unknown[] }> = []
|
|
170
171
|
const fakeClient = {
|