Skip to content

Commit

Permalink
Use a passthrough zod validator for API types (#29475)
Browse files Browse the repository at this point in the history
GitOrigin-RevId: cae8c38695cefb030d99713e310633ffb33c5daf
  • Loading branch information
sujayakar authored and Convex, Inc. committed Sep 3, 2024
1 parent 820d2fa commit 77e786b
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 50 deletions.
18 changes: 11 additions & 7 deletions src/cli/lib/deployApi/checkedComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import {
componentPath,
} from "./paths.js";
import { Identifier, identifier } from "./types.js";
import { looseObject } from "./utils.js";

export const resource = z.union([
z.object({ type: z.literal("value"), value: z.string() }),
z.object({ type: z.literal("function"), path: componentFunctionPath }),
looseObject({ type: z.literal("value"), value: z.string() }),
looseObject({
type: z.literal("function"),
path: componentFunctionPath,
}),
]);
export type Resource = z.infer<typeof resource>;

Expand All @@ -19,23 +23,23 @@ export type CheckedExport =
| { type: "leaf"; resource: Resource };
export const checkedExport: z.ZodType<CheckedExport> = z.lazy(() =>
z.union([
z.object({
looseObject({
type: z.literal("branch"),
children: z.record(identifier, checkedExport),
}),
z.object({
looseObject({
type: z.literal("leaf"),
resource,
}),
]),
);

export const httpActionRoute = z.object({
export const httpActionRoute = looseObject({
method: z.string(),
path: z.string(),
});

export const checkedHttpRoutes = z.object({
export const checkedHttpRoutes = looseObject({
httpModuleRoutes: z.nullable(z.array(httpActionRoute)),
mounts: z.array(z.string()),
});
Expand All @@ -48,7 +52,7 @@ export type CheckedComponent = {
childComponents: Record<Identifier, CheckedComponent>;
};
export const checkedComponent: z.ZodType<CheckedComponent> = z.lazy(() =>
z.object({
looseObject({
definitionPath: componentDefinitionPath,
componentPath,
args: z.record(identifier, resource),
Expand Down
21 changes: 11 additions & 10 deletions src/cli/lib/deployApi/componentDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,30 @@ import { z } from "zod";
import { canonicalizedModulePath, componentDefinitionPath } from "./paths.js";
import { Identifier, Reference, identifier, reference } from "./types.js";
import { analyzedModule, udfConfig } from "./modules.js";
import { looseObject } from "./utils.js";

export const componentArgumentValidator = z.object({
export const componentArgumentValidator = looseObject({
type: z.literal("value"),
// Validator serialized to JSON.
value: z.string(),
});

export const componentDefinitionType = z.union([
z.object({ type: z.literal("app") }),
z.object({
looseObject({ type: z.literal("app") }),
looseObject({
type: z.literal("childComponent"),
name: identifier,
args: z.array(z.tuple([identifier, componentArgumentValidator])),
}),
]);

export const componentArgument = z.object({
export const componentArgument = looseObject({
type: z.literal("value"),
// Value serialized to JSON.
value: z.string(),
});

export const componentInstantiation = z.object({
export const componentInstantiation = looseObject({
name: identifier,
path: componentDefinitionPath,
args: z.nullable(z.array(z.tuple([identifier, componentArgument]))),
Expand All @@ -36,29 +37,29 @@ export type ComponentExports =

export const componentExports: z.ZodType<ComponentExports> = z.lazy(() =>
z.union([
z.object({
looseObject({
type: z.literal("leaf"),
leaf: reference,
}),
z.object({
looseObject({
type: z.literal("branch"),
branch: z.array(z.tuple([identifier, componentExports])),
}),
]),
);

export const componentDefinitionMetadata = z.object({
export const componentDefinitionMetadata = looseObject({
path: componentDefinitionPath,
definitionType: componentDefinitionType,
childComponents: z.array(componentInstantiation),
httpMounts: z.record(z.string(), reference),
exports: z.object({
exports: looseObject({
type: z.literal("branch"),
branch: z.array(z.tuple([identifier, componentExports])),
}),
});

export const evaluatedComponentDefinition = z.object({
export const evaluatedComponentDefinition = looseObject({
definition: componentDefinitionMetadata,
schema: z.any(),
functions: z.record(canonicalizedModulePath, analyzedModule),
Expand Down
5 changes: 3 additions & 2 deletions src/cli/lib/deployApi/definitionConfig.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { z } from "zod";
import { componentDefinitionPath } from "./paths.js";
import { moduleConfig } from "./modules.js";
import { looseObject } from "./utils.js";

export const appDefinitionConfig = z.object({
export const appDefinitionConfig = looseObject({
definition: z.nullable(moduleConfig),
dependencies: z.array(componentDefinitionPath),
schema: z.nullable(moduleConfig),
Expand All @@ -11,7 +12,7 @@ export const appDefinitionConfig = z.object({
});
export type AppDefinitionConfig = z.infer<typeof appDefinitionConfig>;

export const componentDefinitionConfig = z.object({
export const componentDefinitionConfig = looseObject({
definitionPath: componentDefinitionPath,
definition: moduleConfig,
dependencies: z.array(componentDefinitionPath),
Expand Down
15 changes: 8 additions & 7 deletions src/cli/lib/deployApi/modules.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import { z } from "zod";
import { looseObject } from "./utils.js";

export const moduleEnvironment = z.union([
z.literal("isolate"),
z.literal("node"),
]);
export type ModuleEnvironment = z.infer<typeof moduleEnvironment>;

export const moduleConfig = z.object({
export const moduleConfig = looseObject({
path: z.string(),
source: z.string(),
sourceMap: z.optional(z.string()),
environment: moduleEnvironment,
});
export type ModuleConfig = z.infer<typeof moduleConfig>;

export const nodeDependency = z.object({
export const nodeDependency = looseObject({
name: z.string(),
version: z.string(),
});
export type NodeDependency = z.infer<typeof nodeDependency>;

export const udfConfig = z.object({
export const udfConfig = looseObject({
serverVersion: z.string(),
// RNG seed encoded as Convex bytes in JSON.
importPhaseRngSeed: z.any(),
Expand All @@ -33,12 +34,12 @@ export const sourcePackage = z.any();
export type SourcePackage = z.infer<typeof sourcePackage>;

export const visibility = z.union([
z.object({ kind: z.literal("public") }),
z.object({ kind: z.literal("internal") }),
looseObject({ kind: z.literal("public") }),
looseObject({ kind: z.literal("internal") }),
]);
export type Visibility = z.infer<typeof visibility>;

export const analyzedFunction = z.object({
export const analyzedFunction = looseObject({
name: z.string(),
pos: z.any(),
udfType: z.union([
Expand All @@ -52,7 +53,7 @@ export const analyzedFunction = z.object({
});
export type AnalyzedFunction = z.infer<typeof analyzedFunction>;

export const analyzedModule = z.object({
export const analyzedModule = looseObject({
functions: z.array(analyzedFunction),
httpRoutes: z.any(),
cronSpecs: z.any(),
Expand Down
3 changes: 2 additions & 1 deletion src/cli/lib/deployApi/paths.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod";
import { looseObject } from "./utils.js";

// TODO share some of these types, to distinguish between encodedComponentDefinitionPaths etc.
export const componentDefinitionPath = z.string();
Expand All @@ -10,7 +11,7 @@ export type ComponentPath = z.infer<typeof componentPath>;
export const canonicalizedModulePath = z.string();
export type CanonicalizedModulePath = z.infer<typeof canonicalizedModulePath>;

export const componentFunctionPath = z.object({
export const componentFunctionPath = looseObject({
component: z.string(),
udfPath: z.string(),
});
Expand Down
17 changes: 9 additions & 8 deletions src/cli/lib/deployApi/startPush.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import {
componentDefinitionConfig,
} from "./definitionConfig.js";
import { authInfo } from "./types.js";
import { looseObject } from "./utils.js";

export const startPushRequest = z.object({
export const startPushRequest = looseObject({
adminKey: z.string(),
dryRun: z.boolean(),

Expand All @@ -22,13 +23,13 @@ export const startPushRequest = z.object({
});
export type StartPushRequest = z.infer<typeof startPushRequest>;

export const schemaChange = z.object({
export const schemaChange = looseObject({
allocatedComponentIds: z.any(),
schemaIds: z.any(),
});
export type SchemaChange = z.infer<typeof schemaChange>;

export const startPushResponse = z.object({
export const startPushResponse = looseObject({
environmentVariables: z.record(z.string(), z.string()),

externalDepsId: z.nullable(z.string()),
Expand All @@ -43,28 +44,28 @@ export const startPushResponse = z.object({
});
export type StartPushResponse = z.infer<typeof startPushResponse>;

export const componentSchemaStatus = z.object({
export const componentSchemaStatus = looseObject({
schemaValidationComplete: z.boolean(),
indexesComplete: z.number(),
indexesTotal: z.number(),
});
export type ComponentSchemaStatus = z.infer<typeof componentSchemaStatus>;

export const schemaStatus = z.union([
z.object({
looseObject({
type: z.literal("inProgress"),
components: z.record(componentPath, componentSchemaStatus),
}),
z.object({
looseObject({
type: z.literal("failed"),
error: z.string(),
componentPath,
tableName: z.nullable(z.string()),
}),
z.object({
looseObject({
type: z.literal("raceDetected"),
}),
z.object({
looseObject({
type: z.literal("complete"),
}),
]);
Expand Down
3 changes: 2 additions & 1 deletion src/cli/lib/deployApi/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { z } from "zod";
import { looseObject } from "./utils.js";

export const reference = z.string();
export type Reference = z.infer<typeof reference>;

export const authInfo = z.object({
export const authInfo = looseObject({
applicationID: z.string(),
domain: z.string(),
});
Expand Down
37 changes: 37 additions & 0 deletions src/cli/lib/deployApi/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { z } from "zod";

/**
* Convenience wrapper for z.object(...).passthrough().
*
* This object validator allows extra properties and passes them through.
* This is useful for forwards compatibility if the server adds extra unknown
* fields.
*/
export function looseObject<T extends z.ZodRawShape>(
shape: T,
params?: z.RawCreateParams,
): z.ZodObject<
T,
"passthrough",
z.ZodTypeAny,
{
[k_1 in keyof z.objectUtil.addQuestionMarks<
z.baseObjectOutputType<T>,
{
[k in keyof z.baseObjectOutputType<T>]: undefined extends z.baseObjectOutputType<T>[k]
? never
: k;
}[keyof T]
>]: z.objectUtil.addQuestionMarks<
z.baseObjectOutputType<T>,
{
[k in keyof z.baseObjectOutputType<T>]: undefined extends z.baseObjectOutputType<T>[k]
? never
: k;
}[keyof T]
>[k_1];
},
{ [k_2 in keyof z.baseObjectInputType<T>]: z.baseObjectInputType<T>[k_2] }
> {
return z.object(shape, params).passthrough();
}
35 changes: 21 additions & 14 deletions src/cli/lib/deployApi/validator.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { z } from "zod";
import { looseObject } from "./utils.js";

const baseConvexValidator = z.discriminatedUnion("type", [
z.object({ type: z.literal("null") }),
z.object({ type: z.literal("number") }),
z.object({ type: z.literal("bigint") }),
z.object({ type: z.literal("boolean") }),
z.object({ type: z.literal("string") }),
z.object({ type: z.literal("bytes") }),
z.object({ type: z.literal("any") }),
z.object({ type: z.literal("literal"), value: z.any() }),
z.object({ type: z.literal("id"), tableName: z.string() }),
looseObject({ type: z.literal("null") }),
looseObject({ type: z.literal("number") }),
looseObject({ type: z.literal("bigint") }),
looseObject({ type: z.literal("boolean") }),
looseObject({ type: z.literal("string") }),
looseObject({ type: z.literal("bytes") }),
looseObject({ type: z.literal("any") }),
looseObject({ type: z.literal("literal"), value: z.any() }),
looseObject({ type: z.literal("id"), tableName: z.string() }),
]);
export type ConvexValidator =
| z.infer<typeof baseConvexValidator>
Expand All @@ -23,17 +24,23 @@ export type ConvexValidator =
export const convexValidator: z.ZodType<ConvexValidator> = z.lazy(() =>
z.union([
baseConvexValidator,
z.object({ type: z.literal("array"), value: convexValidator }),
z.object({
looseObject({ type: z.literal("array"), value: convexValidator }),
looseObject({
type: z.literal("record"),
keys: convexValidator,
values: convexValidator,
}),
z.object({ type: z.literal("union"), value: z.array(convexValidator) }),
z.object({
looseObject({
type: z.literal("union"),
value: z.array(convexValidator),
}),
looseObject({
type: z.literal("object"),
value: z.record(
z.object({ fieldType: convexValidator, optional: z.boolean() }),
looseObject({
fieldType: convexValidator,
optional: z.boolean(),
}),
),
}),
]),
Expand Down

0 comments on commit 77e786b

Please sign in to comment.