Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
bd86e71
Allow arbitrary JSON in google.protobuf.Struct fields during validation
SkyrimL Mar 19, 2026
937e221
Add compilation test cases. Reuse the same logic in index.ts and sess…
SkyrimL Mar 20, 2026
85bb254
Revert "Add compilation test cases. Reuse the same logic in index.ts …
SkyrimL Mar 23, 2026
d840850
move the unknownToValue logic from session.ts to index.ts and reuse t…
SkyrimL Mar 23, 2026
7e9e4c7
Add compilation test. Add return type.
SkyrimL Mar 23, 2026
670fa35
fix the test error. Using google.protobuf.IValue instead of create th…
SkyrimL Mar 25, 2026
42dbe96
change the import format
SkyrimL Mar 25, 2026
4098a25
add the compilation test
SkyrimL Mar 25, 2026
ca97503
add recursive logic
SkyrimL Mar 25, 2026
da0c071
add a complex test
SkyrimL Mar 25, 2026
b1a54b5
fix a lint bug
SkyrimL Mar 25, 2026
a2bc7b6
fix the test fail
SkyrimL Mar 25, 2026
792f2b5
Refactor verifyObjectMatchesProto with Dynamic Struct Detection (Doub…
SkyrimL Mar 31, 2026
f0e9f6c
fix lint error
SkyrimL Mar 31, 2026
eb4720a
Do incremental conversion only for struct fields
SkyrimL Apr 1, 2026
7e6aed1
fix lint error
SkyrimL Apr 1, 2026
23840da
resolved comments on test
SkyrimL Apr 1, 2026
8a5257c
Add test for struct input already
SkyrimL Apr 2, 2026
8dee580
resolve comments
SkyrimL Apr 2, 2026
3bd115b
remove blank line
SkyrimL Apr 2, 2026
edec4b3
Add glossary terms to the test case for struct already for extraPrope…
SkyrimL Apr 2, 2026
984fd65
refactor the logic to check uncoverted struct and list, add detailed …
SkyrimL Apr 3, 2026
453fa47
refactor the logic to check if a field is given empty by user
SkyrimL Apr 3, 2026
0f09773
remove the logic convert camel case and snake case
SkyrimL Apr 4, 2026
a9a60b8
change proto type from any
SkyrimL Apr 4, 2026
bd504f0
reduce the argument in checkAndConvertFields
SkyrimL Apr 4, 2026
7ca5c49
fix lint error
SkyrimL Apr 4, 2026
c018a6e
Add test
SkyrimL Apr 4, 2026
bdf7ac2
fix lint error
SkyrimL Apr 6, 2026
08b4416
modify import order
SkyrimL Apr 6, 2026
7d90a90
resolve comments about input type
SkyrimL Apr 7, 2026
bfc58e4
resolve comments about index_test
SkyrimL Apr 8, 2026
ddd216c
monkey patching for verify method
SkyrimL Apr 13, 2026
5e75b80
fix lint error
SkyrimL Apr 13, 2026
028a602
have a separate function here that won't be using recursion because v…
SkyrimL Apr 14, 2026
c22d352
get rid of the checkFields function below by recursively patching all…
SkyrimL Apr 14, 2026
a54a42c
fix lint error
SkyrimL Apr 14, 2026
2538d67
fix lint error
SkyrimL Apr 14, 2026
092deec
Revert "fix lint error"
SkyrimL Apr 15, 2026
481a241
Revert "fix lint error"
SkyrimL Apr 15, 2026
de3a926
Revert "get rid of the checkFields function below by recursively patc…
SkyrimL Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions common/protos/BUILD
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
package(default_visibility = ["//visibility:public"])

load("//tools:ts_library.bzl", "ts_library")
load("@df//testing:index.bzl", "ts_test_suite")

ts_library(
name = "protos",
srcs = glob(
["*.ts"],
exclude = ["*_test.ts", "*.spec.ts"],
),
deps = [
"//:modules-fix",
"//common/strings",
"//protos:ts",
"@npm//protobufjs",
],
)

ts_test_suite(
name = "tests",
srcs = glob(["*_test.ts"]),
data = [
"@npm//source-map-support",
],
templated_args = [
"--node_options=--require=source-map-support/register",
"--bazel_patch_module_resolver",
],
deps = [
":protos",
"//protos:ts",
"@df//testing",
"@npm//@types/chai",
"@npm//@types/node",
"@npm//chai",
],
)

load("//testing:build_test.bzl", "build_test")

build_test(
Expand Down
81 changes: 77 additions & 4 deletions common/protos/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { util } from "protobufjs";

import { google } from "df/protos/ts";

const CONFIGS_PROTO_DOCUMENTATION_URL =
"https://dataform-co.github.io/dataform/docs/configs-reference";
const REPORT_ISSUE_URL = "https://github.com/dataform-co/dataform/issues";
Expand All @@ -15,6 +17,8 @@ export interface IProtoClass<IProto, Proto> {
toObject(proto: Proto): { [k: string]: any };
fromObject(obj: { [k: string]: any }): Proto;

verify(obj: any): (string | null);

getTypeUrl(prefix: string): string;
}

Expand All @@ -24,6 +28,24 @@ export enum VerifyProtoErrorBehaviour {
SHOW_DOCS_LINK
}

const Struct = google.protobuf.Struct;

// Save references to the original generated methods
const originalVerify = Struct.verify;

// Monkey Patching Methods
Struct.verify = function (object: any) {
if (object && typeof object === "object" && !("fields" in object)) {
const fields: { [key: string]: any } = {};
for (const [k, v] of Object.entries(object)) {
fields[k] = unknownToValueShallow(v);
}
Object.keys(object).forEach(key => delete object[key]);
object.fields = fields;
}
return originalVerify.call(this, object);
};

// This is a minimalist Typescript equivalent for the validation part of Profobuf's JsonFormat's
// mergeMessage method:
// https://github.com/protocolbuffers/protobuf/blob/670e0c2a0d0b64c994f743a73ee9b8926c47580d/java/util/src/main/java/com/google/protobuf/util/JsonFormat.java#L1455
Expand All @@ -42,6 +64,7 @@ export function verifyObjectMatchesProto<Proto>(
}

// Calling toObject on the object/JSON creates a version only contains the valid proto fields.
protoType.verify(object);
const proto = protoType.create(object);
const protoCastObject = protoType.toObject(proto);

Expand All @@ -58,7 +81,7 @@ export function verifyObjectMatchesProto<Proto>(
if (!presentValue) {
throw ReferenceError(
`Unexpected empty value for "${presentKey}".` +
maybeGetDocsLinkPrefix(errorBehaviour, protoType)
maybeGetDocsLinkPrefix(errorBehaviour, protoType)
);
}
if (typeof presentValue === "object" && Object.keys(presentValue).length === 0) {
Expand All @@ -70,13 +93,13 @@ export function verifyObjectMatchesProto<Proto>(
`Unexpected property "${presentKey}" for "${protoType
.getTypeUrl("")
.replace("/", "")}", please report this to the Dataform team at ` +
`${REPORT_ISSUE_URL}.`
`${REPORT_ISSUE_URL}.`
);
}
throw ReferenceError(
`Unexpected property "${presentKey}", or property value type of ` +
`"${typeof presentValue}" is incorrect.` +
maybeGetDocsLinkPrefix(errorBehaviour, protoType)
`"${typeof presentValue}" is incorrect.` +
maybeGetDocsLinkPrefix(errorBehaviour, protoType)
);
}
if (typeof presentValue === "object") {
Expand Down Expand Up @@ -140,3 +163,53 @@ function fromBase64(value: string): Uint8Array {
util.base64.decode(value, buf, 0);
return buf;
}

function unknownToValueShallow(raw: unknown): google.protobuf.IValue {
if (raw === null || typeof raw === "undefined") {
return { nullValue: 0 };
}
if (typeof raw === "string") {
return { stringValue: raw };
}
if (typeof raw === "number") {
return { numberValue: raw };
}
if (typeof raw === "boolean") {
return { boolValue: raw };
}
if (Array.isArray(raw)) {
return { listValue: { values: raw.map(unknownToValueShallow) } };
}
if (typeof raw === "object") {
return { structValue: raw as any };
}
throw new Error(`Unsupported value: ${raw}`);
}

export function unknownToValue(raw: unknown): google.protobuf.IValue {
if (raw === null || typeof raw === "undefined") {
return { nullValue: 0 };
}
if (typeof raw === "string") {
return { stringValue: raw };
}
if (typeof raw === "number") {
return { numberValue: raw };
}
if (typeof raw === "boolean") {
return { boolValue: raw };
}
if (Array.isArray(raw)) {
return { listValue: { values: raw.map(unknownToValue) } };
}
if (typeof raw === "object") {
return {
structValue: {
fields: Object.fromEntries(
Object.entries(raw).map(([key, value]) => [key, unknownToValue(value)])
)
}
};
}
throw new Error(`Unsupported value: ${raw}`);
}
45 changes: 45 additions & 0 deletions common/protos/index_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { expect } from "chai";
import { verifyObjectMatchesProto, VerifyProtoErrorBehaviour } from "./index";

import { dataform } from "df/protos/ts";
import { suite, test } from "df/testing";

suite("verifyObjectMatchesProto", () => {
test("throws error when top-level object is an array", () => {
expect(() => {
verifyObjectMatchesProto(dataform.Target, [] as any);
}).to.throw(ReferenceError, "Expected a top-level object, but found an array");
});

test("throws error when null value provided for array field and SHOW_DOCS_LINK", () => {
expect(() => {
verifyObjectMatchesProto(
dataform.Table,
{ dependencyTargets: null } as any,
VerifyProtoErrorBehaviour.SHOW_DOCS_LINK
);
}).to.throw(ReferenceError, /Unexpected empty value for "dependencyTargets"/);
});

test("throws error on type mismatch with SUGGEST_REPORTING_TO_DATAFORM_TEAM", () => {
expect(() => {
verifyObjectMatchesProto(
dataform.Table,
{ actionDescriptor: 123 } as any,
VerifyProtoErrorBehaviour.SUGGEST_REPORTING_TO_DATAFORM_TEAM
);
}).to.throw(
ReferenceError,
/Unexpected property "actionDescriptor" for ".*Table".*please report this to the Dataform team/
);
});

test("throws error on default type mismatch", () => {
expect(() => {
verifyObjectMatchesProto(dataform.Table, { actionDescriptor: 123 } as any);
}).to.throw(
ReferenceError,
/Unexpected property "actionDescriptor", or property value type of "number" is incorrect/
);
});
});
Loading
Loading