diff --git a/.vscode/settings.json b/.vscode/settings.json index 4c1f415..6c981d4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "codecov", "connectrpc", "displaystatemap", + "instanceof", "keyof", "morty", "njwt", diff --git a/README.md b/README.md index f8846a9..a55ef4b 100644 --- a/README.md +++ b/README.md @@ -471,6 +471,23 @@ Get an object instance with the type `type-name` and the id `object-id`. For exa ```typescript const user = await directoryClient.object({ objectType: 'user', objectId: 'euang@acmecorp.com' }); + +// Handle a not found object +import { NotFoundError } from "@aserto/aserto-node" + +try { + directoryClient.object({ + objectType: "user", + objectId: "euang@acmecorp.com", + }); +} catch (error) { + if (error instanceof NotFoundError) { + // pass trough + } + + // throw back the original error + throw error; +} ``` #### 'relation' function diff --git a/__tests__/directory/v3/index.test.ts b/__tests__/directory/v3/index.test.ts index aaafa7a..09596e5 100644 --- a/__tests__/directory/v3/index.test.ts +++ b/__tests__/directory/v3/index.test.ts @@ -234,7 +234,7 @@ describe("DirectoryV3", () => { it("handles ConnectError", async () => { const mockCheckPermission = jest .spyOn(directory.ReaderClient, "checkPermission") - .mockRejectedValue(new ConnectError("connect error", 5)); + .mockRejectedValue(new ConnectError("connect error", 1)); const params = { subjectId: "euang@acmecorp.com", @@ -244,7 +244,7 @@ describe("DirectoryV3", () => { objectId: "admin", }; await expect(directory.checkPermission(params)).rejects.toThrow( - '"checkPermission" failed with code: 5, message: [not_found] connect error' + '"checkPermission" failed with code: 1, message: [canceled] connect error' ); mockCheckPermission.mockReset(); diff --git a/__tests__/integration/index.test.ts b/__tests__/integration/index.test.ts index d097ae3..136b8c7 100644 --- a/__tests__/integration/index.test.ts +++ b/__tests__/integration/index.test.ts @@ -4,7 +4,9 @@ import { createAsyncIterable, DirectoryServiceV3, DirectoryV3, + EtagMismatchError, getSSLCredentials, + NotFoundError, policyInstance, readAsyncIterable, } from "../../lib"; @@ -94,6 +96,19 @@ types: ).resolves.not.toThrow(); }); + xit("throws EtagMismatchError when setting the same object without Etag", async () => { + await expect( + directoryClient.setObject({ + object: { + type: "user", + id: "test-user", + displayName: "updated", + etag: "updated", + }, + }) + ).rejects.toThrow(EtagMismatchError); + }); + it("sets a another object", async () => { await expect( directoryClient.setObject({ @@ -255,7 +270,7 @@ types: ).resolves.not.toThrow(); }); - it("throws error when getting a delete relation", async () => { + it("throws NotFoundError when getting a delete relation", async () => { await expect( directoryClient.relation({ subjectId: "test-user", @@ -264,9 +279,7 @@ types: objectId: "test-group", objectType: "group", }) - ).rejects.toThrow( - '"relation" failed with code: 5, message: [not_found] E20051 key not found' - ); + ).rejects.toThrow(NotFoundError); }); it("list user objects", async () => { @@ -313,20 +326,16 @@ types: ).resolves.not.toThrow(); }); - it("throws error when getting a deleted user object", async () => { + it("throws NotFoundError when getting a deleted user object", async () => { await expect( directoryClient.object({ objectType: "user", objectId: "test-user" }) - ).rejects.toThrow( - '"object" failed with code: 5, message: [not_found] E20051 key not found' - ); + ).rejects.toThrow(NotFoundError); }); - it("throws error when getting a deleted group object", async () => { + it("throws NotFoundError when getting a deleted group object", async () => { await expect( directoryClient.object({ objectType: "group", objectId: "test-group" }) - ).rejects.toThrow( - '"object" failed with code: 5, message: [not_found] E20051 key not found' - ); + ).rejects.toThrow(NotFoundError); }); it("returns [] when there are no objects", async () => { @@ -432,26 +441,22 @@ types: ).resolves.not.toThrow(); }); - it("throws error when getting a deleted user object", async () => { + it("throws NotFoundError when getting a deleted user object", async () => { await expect( directoryClient.object({ objectType: "user", objectId: "import-user" }) - ).rejects.toThrow( - '"object" failed with code: 5, message: [not_found] E20051 key not found' - ); + ).rejects.toThrow(NotFoundError); }); - it("throws error when getting a deleted group object", async () => { + it("throws NotFoundError when getting a deleted group object", async () => { await expect( directoryClient.object({ objectType: "group", objectId: "import-group", }) - ).rejects.toThrow( - '"object" failed with code: 5, message: [not_found] E20051 key not found' - ); + ).rejects.toThrow(NotFoundError); }); - it("throws error when getting a delete relation", async () => { + it("throws NotFoundError when getting a delete relation", async () => { await expect( directoryClient.relation({ subjectId: "import-user", @@ -460,9 +465,7 @@ types: objectId: "import-group", objectType: "group", }) - ).rejects.toThrow( - '"relation" failed with code: 5, message: [not_found] E20051 key not found' - ); + ).rejects.toThrow(NotFoundError); }); }); diff --git a/lib/directory/errors.ts b/lib/directory/errors.ts new file mode 100644 index 0000000..36a7859 --- /dev/null +++ b/lib/directory/errors.ts @@ -0,0 +1,33 @@ +/** + * An Error return when a directory Object or Relation is not found. + * Extends the built-in Error class. + * + * @class NotFoundError + * @extends Error + */ +export class NotFoundError extends Error {} +/** + * "Invalid Argument" error. + * Extends the built-in Error class. + * + * @class InvalidArgumentError + * @extends Error + */ +export class InvalidArgumentError extends Error {} +/** + * "Etag Mismatch" error. + * Extends the built-in Error class. + * + * @class EtagMismatchError + * @extends Error + */ +export class EtagMismatchError extends Error {} +/** + * "Unauthenticated" error. + * Extends the built-in Error class. + * + * @class EtagMismatchError + * @extends Error + */ +export class UnauthenticatedError extends Error {} +export class ServiceError extends Error {} diff --git a/lib/directory/v3/index.ts b/lib/directory/v3/index.ts index 761aef4..1e20be3 100644 --- a/lib/directory/v3/index.ts +++ b/lib/directory/v3/index.ts @@ -24,6 +24,7 @@ import { Struct, } from "@bufbuild/protobuf"; import { + Code, ConnectError, createPromiseClient, Interceptor, @@ -33,6 +34,13 @@ import { } from "@connectrpc/connect"; import { createGrpcTransport } from "@connectrpc/connect-node"; +import { + EtagMismatchError, + InvalidArgumentError, + NotFoundError, + ServiceError, + UnauthenticatedError, +} from "../errors"; import { CheckPermissionRequest, CheckRelationRequest, @@ -465,9 +473,27 @@ export async function* createAsyncIterable(items: T[]): AsyncIterable { function handleError(error: unknown, method: string) { if (error instanceof ConnectError) { - throw new Error( - `"${method}" failed with code: ${error.code}, message: ${error.message}` - ); + switch (error.code) { + case Code.Unauthenticated: { + throw new UnauthenticatedError( + `Authentication failed: ${error.message}` + ); + } + case Code.NotFound: { + throw new NotFoundError(`${method} not found`); + } + case Code.InvalidArgument: { + throw new InvalidArgumentError(`${method}: ${error.message}`); + } + case Code.FailedPrecondition: { + throw new EtagMismatchError(`invalid etag in ${method} request`); + } + default: { + throw new ServiceError( + `"${method}" failed with code: ${error.code}, message: ${error.message}` + ); + } + } } else { throw error; } diff --git a/lib/index.ts b/lib/index.ts index 63a02f8..84b10a8 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -58,3 +58,5 @@ export { SubIdentityMapper, DirectoryConfig as ServiceConfig, }; + +export * from "./directory/errors";