Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
221 changes: 220 additions & 1 deletion src/v1.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as grpc from "@grpc/grpc-js";
import {} from "@grpc/grpc-js";
Comment thread
jevy marked this conversation as resolved.
Outdated
import { generateTestToken } from "./__utils__/helpers.js";
import { Struct } from "./authzedapi/google/protobuf/struct.js";
import { PreconnectServices, deadlineInterceptor } from "./util.js";
Expand All @@ -18,15 +19,18 @@ import {
LookupSubjectsResponse,
NewClient,
ObjectReference,
PermissionsServiceClient,
Relationship,
RelationshipUpdate,
RelationshipUpdate_Operation,
SubjectReference,
WriteRelationshipsRequest,
WriteRelationshipsResponse,
WriteSchemaRequest,
createStructFromObject,
PbNullValue,
} from "./v1.js";
import { describe, it, expect, beforeEach } from "vitest";
import { describe, it, expect, beforeEach, vi } from "vitest";

describe("a check with an unknown namespace", () => {
it("should raise a failed precondition", () =>
Expand Down Expand Up @@ -681,4 +685,219 @@ describe("Experimental Service", () => {
});
});
}));

describe("WriteRelationships with transaction metadata (Integration Test)", () => {
it("should successfully write relationships with metadata and verify metadata transmission", () =>
new Promise<void>((done, fail) => {
const testToken = generateTestToken("v1-int-tx-metadata");
const client = NewClient(
testToken,
"localhost:50051",
ClientSecurity.INSECURE_LOCALHOST_ALLOWED,
PreconnectServices.SCHEMA_SERVICE |
PreconnectServices.PERMISSIONS_SERVICE,
);

const writeSpy = vi.spyOn(
PermissionsServiceClient.prototype,
"writeRelationships",
);

const schema = `
definition test/user {}
definition test/document {
relation viewer: test/user
permission view = viewer
}
`;
const writeSchemaRequest = WriteSchemaRequest.create({ schema });

client.writeSchema(writeSchemaRequest, (schemaErr, schemaResponse) => {
if (schemaErr) {
client.close();
fail(schemaErr);
return;
}
expect(schemaResponse).toBeDefined();

const uniqueSuffix = Date.now();
const resource = ObjectReference.create({
objectType: "test/document",
objectId: `doc-${uniqueSuffix}`,
});

const user = ObjectReference.create({
objectType: "test/user",
objectId: `user-${uniqueSuffix}`,
});

const updates = [
RelationshipUpdate.create({
relationship: Relationship.create({
resource,
relation: "viewer",
subject: SubjectReference.create({ object: user }),
}),
operation: RelationshipUpdate_Operation.CREATE,
}),
];

const metadataObject = {
transaction_id: "test-tx-123",
other_data: "sample",
};
const transactionMetadata = createStructFromObject(metadataObject);

const writeRequest = WriteRelationshipsRequest.create({
updates,
optionalTransactionMetadata: transactionMetadata,
});

client.writeRelationships(writeRequest, (err, response) => {
if (err) {
client.close();
fail(err);
return;
}

expect(err).toBeNull();
expect(response).toBeDefined();
expect(response?.writtenAt).toBeDefined();

expect(writeSpy).toHaveBeenCalledTimes(1);

const actualRequest = writeSpy.mock
.calls[0][0] as WriteRelationshipsRequest;

expect(actualRequest.updates).toEqual(updates);

expect(actualRequest.optionalTransactionMetadata).toBeDefined();
expect(actualRequest.optionalTransactionMetadata).toEqual(
transactionMetadata,
);

const transactionIdField =
actualRequest.optionalTransactionMetadata?.fields?.[
"transaction_id"
];
expect(transactionIdField?.kind?.oneofKind).toBe("stringValue");
if (transactionIdField?.kind?.oneofKind === "stringValue") {
expect(transactionIdField.kind.stringValue).toBe("test-tx-123");
}

const otherDataField =
actualRequest.optionalTransactionMetadata?.fields?.["other_data"];
expect(otherDataField?.kind?.oneofKind).toBe("stringValue");
if (otherDataField?.kind?.oneofKind === "stringValue") {
expect(otherDataField.kind.stringValue).toBe("sample");
}

client.close();
done();
});
});
}));
});
});

describe("createStructFromObject unit tests", () => {
it("should convert a simple JS object with primitive types", () => {
const obj = {
stringProp: "hello",
numberProp: 123,
booleanProp: true,
};
const struct = createStructFromObject(obj);
expect(struct.fields.stringProp?.kind.oneofKind).toBe("stringValue");
expect(
struct.fields.stringProp?.kind.oneofKind === "stringValue" &&
struct.fields.stringProp?.kind.stringValue,
).toBe("hello");
expect(struct.fields.numberProp?.kind.oneofKind).toBe("numberValue");
expect(
struct.fields.numberProp?.kind.oneofKind === "numberValue" &&
struct.fields.numberProp?.kind.numberValue,
).toBe(123);
expect(struct.fields.booleanProp?.kind.oneofKind).toBe("boolValue");
expect(
struct.fields.booleanProp?.kind.oneofKind === "boolValue" &&
struct.fields.booleanProp?.kind.boolValue,
).toBe(true);
});

it("should convert a JS object with null values", () => {
const obj = {
nullProp: null,
};
const struct = createStructFromObject(obj);
expect(struct.fields.nullProp?.kind.oneofKind).toBe("nullValue");
expect(
struct.fields.nullProp?.kind.oneofKind === "nullValue" &&
struct.fields.nullProp?.kind.nullValue,
).toBe(PbNullValue.NULL_VALUE);
});

it("should convert a JS object with nested objects", () => {
const obj = {
nestedProp: {
innerString: "world",
innerNumber: 456,
},
};
const struct = createStructFromObject(obj);
const nestedStruct =
struct.fields.nestedProp?.kind.oneofKind === "structValue" &&
struct.fields.nestedProp.kind.structValue;
expect(nestedStruct).toBeTruthy();
if (nestedStruct) {
expect(nestedStruct.fields.innerString?.kind.oneofKind).toBe(
"stringValue",
);
expect(
nestedStruct.fields.innerString?.kind.oneofKind === "stringValue" &&
nestedStruct.fields.innerString?.kind.stringValue,
).toBe("world");
expect(nestedStruct.fields.innerNumber?.kind.oneofKind).toBe(
"numberValue",
);
expect(
nestedStruct.fields.innerNumber?.kind.oneofKind === "numberValue" &&
nestedStruct.fields.innerNumber?.kind.numberValue,
).toBe(456);
}
});

it("should convert an empty JS object to an empty Struct", () => {
const obj = {};
const struct = createStructFromObject(obj);
expect(Object.keys(struct.fields).length).toBe(0);
});

it("should throw an error for null input", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => createStructFromObject(null as any)).toThrow(
"Input data for createStructFromObject must be a non-null object.",
);
});

it("should throw an error for non-object input (string)", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => createStructFromObject("not an object" as any)).toThrow(
"Input data for createStructFromObject must be a non-null object.",
);
});

it("should throw an error for non-object input (number)", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => createStructFromObject(123 as any)).toThrow(
"Input data for createStructFromObject must be a non-null object.",
);
});

it("should throw an error for array input", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => createStructFromObject([] as any)).toThrow(
"Input data for createStructFromObject must be a non-null object.",
);
});
});
23 changes: 23 additions & 0 deletions src/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ import {
} from "./util.js";

import type { OmitBaseMethods, PromisifiedClient } from "./types.js";
import {
Struct as ImportedPbStruct,
NullValue as ImportedPbNullValue,
} from "./authzedapi/google/protobuf/struct.js";
import type { JsonObject } from "@protobuf-ts/runtime";

export { ImportedPbStruct as PbStruct, ImportedPbNullValue as PbNullValue };

// A merge of the three generated gRPC clients, with their base methods omitted
export type ZedDefaultClientInterface = OmitBaseMethods<
Expand Down Expand Up @@ -344,6 +351,22 @@ export function NewClientWithChannelCredentials(
): ZedClientInterface {
return ZedCombinedClient.create(endpoint, creds, preconnect, options);
}
/**
* Creates a google.protobuf.Struct object suitable for use as
* optionalTransactionMetadata in WriteRelationshipsRequest.
*
* @param data A simple JavaScript object (e.g., { key: "value" }) to be converted into a Struct.
* @returns A google.protobuf.Struct object.
*/
export function createStructFromObject(data: JsonObject): ImportedPbStruct {
if (data === null || typeof data !== "object" || Array.isArray(data)) {
// Or handle this case as per library's error handling philosophy
throw new Error(
"Input data for createStructFromObject must be a non-null object.",
);
}
Comment thread
jevy marked this conversation as resolved.
Outdated
return ImportedPbStruct.fromJson(data);
}

export * from "./authzedapi/authzed/api/v1/core.js";
export * from "./authzedapi/authzed/api/v1/experimental_service.js";
Expand Down