Skip to content

Commit dbf142f

Browse files
authored
fix:(js/core/schema): Allow disabling runtime schema compilation (#3988)
1 parent bce221e commit dbf142f

File tree

6 files changed

+135
-43
lines changed

6 files changed

+135
-43
lines changed

‎js/core/package.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,21 @@
2929
"@opentelemetry/api": "^1.9.0",
3030
"@opentelemetry/context-async-hooks": "~1.25.0",
3131
"@opentelemetry/core": "~1.25.0",
32+
"@opentelemetry/exporter-jaeger": "^1.25.0",
3233
"@opentelemetry/sdk-metrics": "~1.25.0",
3334
"@opentelemetry/sdk-node": "^0.52.0",
3435
"@opentelemetry/sdk-trace-base": "~1.25.0",
35-
"@opentelemetry/exporter-jaeger": "^1.25.0",
3636
"@types/json-schema": "^7.0.15",
3737
"ajv": "^8.12.0",
3838
"ajv-formats": "^3.0.1",
3939
"async-mutex": "^0.5.0",
40-
"body-parser": "^1.20.3",
4140
"cors": "^2.8.5",
41+
"dotprompt": "^1.1.1",
4242
"express": "^4.21.0",
4343
"get-port": "^5.1.0",
4444
"json-schema": "^0.4.0",
4545
"zod": "^3.23.8",
46-
"zod-to-json-schema": "^3.22.4",
47-
"dotprompt": "^1.1.1"
46+
"zod-to-json-schema": "^3.22.4"
4847
},
4948
"devDependencies": {
5049
"@types/express": "^4.17.21",
@@ -57,6 +56,7 @@
5756
"typescript": "^4.9.0"
5857
},
5958
"optionalDependencies": {
59+
"@cfworker/json-schema": "^4.1.1",
6060
"@genkit-ai/firebase": "^1.16.1"
6161
},
6262
"types": "lib/index.d.ts",

‎js/core/src/index.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,12 @@ export {
7777
} from './flow.js';
7878
export * from './plugin.js';
7979
export * from './reflection.js';
80-
export { defineJsonSchema, defineSchema, type JSONSchema } from './schema.js';
80+
export {
81+
defineJsonSchema,
82+
defineSchema,
83+
disableSchemaCodeGeneration,
84+
type JSONSchema,
85+
} from './schema.js';
8186
export * from './telemetryTypes.js';
8287
export * from './utils.js';
8388

‎js/core/src/schema.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,36 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { Validator } from '@cfworker/json-schema';
1718
import Ajv, { type ErrorObject, type JSONSchemaType } from 'ajv';
1819
import addFormats from 'ajv-formats';
1920
import { z } from 'zod';
2021
import zodToJsonSchema from 'zod-to-json-schema';
2122
import { GenkitError } from './error.js';
23+
import { logger } from './logging.js';
2224
import type { Registry } from './registry.js';
2325
const ajv = new Ajv();
2426
addFormats(ajv);
2527

28+
const SCHEMA_VALIDATION_MODE = 'schemaValidationMode' as const;
29+
30+
/**
31+
* Disable schema code generation in runtime. Use this if your runtime
32+
* environment restricts the use of `eval` or `new Function`, for e.g., in
33+
* CloudFlare workers.
34+
*/
35+
export function disableSchemaCodeGeneration() {
36+
logger.warn(
37+
"It looks like you're trying to disable schema code generation. Please ensure that the '@cfworker/json-schema' package is installed: `npm i --save @cfworker/json-schema`"
38+
);
39+
global[SCHEMA_VALIDATION_MODE] = 'interpret';
40+
}
41+
42+
/** Visible for testing */
43+
export function resetSchemaCodeGeneration() {
44+
global[SCHEMA_VALIDATION_MODE] = undefined;
45+
}
46+
2647
export { z }; // provide a consistent zod to use throughout genkit
2748

2849
/**
@@ -32,6 +53,7 @@ export type JSONSchema = JSONSchemaType<any> | any;
3253

3354
const jsonSchemas = new WeakMap<z.ZodTypeAny, JSONSchema>();
3455
const validators = new WeakMap<JSONSchema, ReturnType<typeof ajv.compile>>();
56+
const cfWorkerValidators = new WeakMap<JSONSchema, Validator>();
3557

3658
/**
3759
* Wrapper object for various ways schema can be provided.
@@ -97,6 +119,19 @@ function toErrorDetail(error: ErrorObject): ValidationErrorDetail {
97119
};
98120
}
99121

122+
function cfWorkerErrorToValidationErrorDetail(error: {
123+
instanceLocation: string;
124+
error: string;
125+
}): ValidationErrorDetail {
126+
const path = error.instanceLocation.startsWith('#/')
127+
? error.instanceLocation.substring(2)
128+
: '';
129+
return {
130+
path: path.replace(/\//g, '.') || '(root)',
131+
message: error.error,
132+
};
133+
}
134+
100135
/**
101136
* Validation response.
102137
*/
@@ -115,6 +150,24 @@ export function validateSchema(
115150
if (!toValidate) {
116151
return { valid: true, schema: toValidate };
117152
}
153+
const validationMode = (global[SCHEMA_VALIDATION_MODE] ?? 'compile') as
154+
| 'compile'
155+
| 'interpret';
156+
157+
if (validationMode === 'interpret') {
158+
let validator = cfWorkerValidators.get(toValidate);
159+
if (!validator) {
160+
validator = new Validator(toValidate);
161+
cfWorkerValidators.set(toValidate, validator);
162+
}
163+
const result = validator.validate(data);
164+
return {
165+
valid: result.valid,
166+
errors: result.errors?.map(cfWorkerErrorToValidationErrorDetail),
167+
schema: toValidate,
168+
};
169+
}
170+
118171
const validator = validators.get(toValidate) || ajv.compile(toValidate);
119172
const valid = validator(data) as boolean;
120173
const errors = validator.errors?.map((e) => e);

‎js/core/tests/schema_test.ts‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414
* limitations under the License.
1515
*/
1616

17+
import Ajv from 'ajv';
1718
import * as assert from 'assert';
18-
import { describe, it } from 'node:test';
19+
import { describe, it, mock } from 'node:test';
1920

2021
import {
2122
ValidationError,
23+
disableSchemaCodeGeneration,
2224
parseSchema,
25+
resetSchemaCodeGeneration,
2326
toJsonSchema,
2427
validateSchema,
2528
z,
@@ -162,3 +165,27 @@ describe('toJsonSchema', () => {
162165
);
163166
});
164167
});
168+
169+
describe('disableSchemaCodeGeneration()', () => {
170+
it('should validate using cfworker validator', () => {
171+
const compileMock = mock.method(Ajv.prototype, 'compile');
172+
173+
disableSchemaCodeGeneration();
174+
const result = validateSchema(
175+
{ foo: 123 },
176+
{
177+
jsonSchema: {
178+
type: 'object',
179+
properties: { foo: { type: 'boolean' } },
180+
},
181+
}
182+
);
183+
184+
assert.strictEqual(result.valid, false);
185+
const errorAtFoo = result.errors?.find((e) => e.path === 'foo');
186+
assert.ok(errorAtFoo, 'Should have error at foo');
187+
assert.strictEqual(compileMock.mock.callCount(), 0);
188+
compileMock.mock.restore();
189+
resetSchemaCodeGeneration();
190+
});
191+
});

‎js/genkit/src/common.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export {
129129
UserFacingError,
130130
defineJsonSchema,
131131
defineSchema,
132+
disableSchemaCodeGeneration,
132133
getClientHeader,
133134
getCurrentEnv,
134135
getStreamingCallback,

0 commit comments

Comments
 (0)