Skip to content

Commit

Permalink
Merge pull request #141 from nitrictech/api-details
Browse files Browse the repository at this point in the history
Implement API details
  • Loading branch information
tjholm authored Nov 8, 2022
2 parents 8362824 + 6c699e2 commit b7c744b
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 15 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
],
"dependencies": {
"@grpc/grpc-js": "^1.5.9",
"@nitric/api": "v0.19.0",
"@nitric/api": "v0.20.0-rc.2",
"google-protobuf": "3.14.0",
"tslib": "^2.1.0"
},
Expand Down
83 changes: 77 additions & 6 deletions src/resources/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
// limitations under the License.
import * as faas from "../faas/index";
import { api, ApiWorkerOptions } from '.';
import { ResourceServiceClient } from "@nitric/api/proto/resource/v1/resource_grpc_pb";
import { ApiResourceDetails, ResourceDetailsResponse } from "@nitric/api/proto/resource/v1/resource_pb";

jest.mock('../faas/index');

Expand All @@ -39,12 +41,12 @@ describe('Api', () => {
it("should create a new FaasClient", () => {
expect(faas.Faas).toBeCalledTimes(1);
});

it("should provide Faas with ApiWorkerOptions", () => {
const expectedOpts = new ApiWorkerOptions("main", "/newroute/", ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS']);
expect(faas.Faas).toBeCalledWith(expectedOpts)
});

it("should call FaasClient::start()", () => {
expect(startSpy).toBeCalledTimes(1);
});
Expand All @@ -61,25 +63,94 @@ describe('Api', () => {
},
})
});

afterAll(() => {
jest.resetAllMocks();
});

it("should create a new FaasClient", () => {
expect(faas.Faas).toBeCalledTimes(1);
});

it("should provide Faas with ApiWorkerOptions", () => {
const expectedOpts = new ApiWorkerOptions("main", "/test/", [method.toUpperCase() as any], {
security: { "test": [] }
});
expect(faas.Faas).toBeCalledWith(expectedOpts)
});

it("should call FaasClient::start()", () => {
expect(startSpy).toBeCalledTimes(1);
});
});
});

describe("when getting the url", () => {
describe("when api details are returned", () => {
let a;
let detailsSpy;

beforeAll(async () => {
// mock the details api
detailsSpy = jest
.spyOn(ResourceServiceClient.prototype, 'details')
.mockImplementationOnce((request, callback: any) => {
const resp = new ResourceDetailsResponse();
resp.setId("mock-id");
resp.setProvider("mock-provider");
resp.setService("mock-service");


const api = new ApiResourceDetails();
api.setUrl("http://localhost:9001/test");
resp.setApi(api);

callback(null, resp);

return null as any;
});

a = await api("main");
});

afterAll(() => {
jest.resetAllMocks();
});

it("should return the url", async () => {
await expect(a.url()).resolves.toBe("http://localhost:9001/test");
});
});

describe("when non api details are returned", () => {
let a;
let detailsSpy;

beforeAll(async () => {
// mock the details api
detailsSpy = jest
.spyOn(ResourceServiceClient.prototype, 'details')
.mockImplementationOnce((request, callback: any) => {
const resp = new ResourceDetailsResponse();
resp.setId("mock-id");
resp.setProvider("mock-provider");
resp.setService("mock-service");

callback(null, resp);

return null as any;
});

a = await api("main");
});

afterAll(() => {
jest.resetAllMocks();
});

it("should throw an error", async () => {
await expect(a.url()).rejects.toThrowError("Unexpected details in response. Expected API details");
});
});
});
});
35 changes: 33 additions & 2 deletions src/resources/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import {
Resource,
ResourceDeclareRequest,
ResourceDeclareResponse,
ResourceDetailsRequest,
ResourceDetailsResponse,
ResourceType,
ResourceTypeMap,
} from '@nitric/api/proto/resource/v1/resource_pb';
import { fromGrpcError } from '../api/errors';
import resourceClient from './client';
Expand Down Expand Up @@ -196,12 +199,16 @@ interface ApiOpts<Defs extends string> {
security?: Record<Defs, string[]>;
}

interface ApiDetails {
url: string;
}

/**
* API Resource
*
* Represents an HTTP API, capable of routing and securing incoming HTTP requests to handlers.
*/
class Api<SecurityDefs extends string> extends Base {
class Api<SecurityDefs extends string> extends Base<ApiDetails> {
// public readonly name: string;
public readonly path: string;
public readonly middleware?: HttpMiddleware[];
Expand Down Expand Up @@ -318,7 +325,31 @@ class Api<SecurityDefs extends string> extends Base {
}

/**
* Register this bucket as a required resource for the calling function/container
* Retrieves the Invocation URL of this API at runtime
* @returns {Promise} that contains the url of this API
*/
async url(): Promise<string> {
const { details: { url } } = await this.details();

return url;
}

protected resourceType(): ResourceTypeMap[keyof ResourceTypeMap] {
return ResourceType.API;
}

protected unwrapDetails(resp: ResourceDetailsResponse): ApiDetails {
if (resp.hasApi()) {
return {
url: resp.getApi().getUrl()
}
}

throw new Error("Unexpected details in response. Expected API details")
}

/**
* Register this api as a required resource for the calling function/container
* @returns a promise that resolves when the registration is complete
*/
protected async register(): Promise<Resource> {
Expand Down
9 changes: 9 additions & 0 deletions src/resources/bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Resource,
ResourceDeclareRequest,
ResourceDeclareResponse,
ResourceDetailsResponse,
ResourceType,
} from '@nitric/api/proto/resource/v1/resource_pb';
import resourceClient from './client';
Expand Down Expand Up @@ -76,6 +77,14 @@ export class BucketResource extends SecureResource<BucketPermission> {
}, []);
}

protected resourceType() {
return ResourceType.BUCKET;
}

protected unwrapDetails(resp: ResourceDetailsResponse): {} {
throw new Error("details unimplemented for bucket");
}

/**
* Return a bucket reference and register the permissions required by the currently scoped function for this resource.
*
Expand Down
9 changes: 9 additions & 0 deletions src/resources/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ResourceDeclareResponse,
ResourceType,
Action,
ResourceDetailsResponse,
} from '@nitric/api/proto/resource/v1/resource_pb';
import { fromGrpcError } from '../api/errors';
import { documents } from '../api/documents';
Expand Down Expand Up @@ -87,6 +88,14 @@ export class CollectionResource<T extends DocumentStructure> extends SecureResou
return actions;
}

protected resourceType() {
return ResourceType.COLLECTION;
}

protected unwrapDetails(resp: ResourceDetailsResponse): {} {
throw new Error("details unimplemented for collection");
}

/**
* Return a collection reference and register the permissions required by the currently scoped function for this resource.
*
Expand Down
55 changes: 54 additions & 1 deletion src/resources/common.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Resource, make } from "./common";
import { ResourceServiceClient } from "@nitric/api/proto/resource/v1/resource_grpc_pb";
import { ResourceDetailsResponse } from "@nitric/api/proto/resource/v1/resource_pb";
import { Resource, make, ResourceDetails } from "./common";

const MOCK_RESOURCE = 0;
const MOCK_DETAILS = {};

class MockResource extends Resource {
register = jest.fn().mockReturnValue(Promise.resolve());
permsToActions = jest.fn();
unwrapDetails = jest.fn(() => MOCK_DETAILS);
resourceType = jest.fn(() => MOCK_RESOURCE) as any;
}


Expand All @@ -43,5 +49,52 @@ describe('common', () => {
expect(test1).toStrictEqual(test2);
})
})

describe("when calling resource details", () => {
const test = res("same");

let detailsSpy;
let details: ResourceDetails<{}>;

beforeAll(async () => {
detailsSpy = jest
.spyOn(ResourceServiceClient.prototype, 'details')
.mockImplementationOnce((request, callback: any) => {
const resp = new ResourceDetailsResponse()
resp.setId("mock-id");
resp.setProvider("mock-provider");
resp.setService("mock-service");
callback(null, resp);

return null as any;
});

details = await test['details']();
});

afterAll(() => {
detailsSpy.mockClear();
});

it("should call gRPC details method", () => {
expect(detailsSpy).toBeCalledTimes(1);
});

it("should return unwrapped details", () => {
expect(details.details).toBe(MOCK_DETAILS);
});

it("should return the correct id", () => {
expect(details.id).toBe("mock-id");
});

it("should return the correct provider", () => {
expect(details.provider).toBe("mock-provider");
});

it("should return the correct service", () => {
expect(details.service).toBe("mock-service");
});
});
})
});
42 changes: 41 additions & 1 deletion src/resources/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,23 @@ import {
PolicyResource,
ResourceDeclareRequest,
ActionMap,
ResourceDetailsRequest,
ResourceDetailsResponse,
ResourceTypeMap,
} from '@nitric/api/proto/resource/v1/resource_pb';
import resourceClient from './client';
import { fromGrpcError } from '../api/errors';

export type ActionsList = ActionMap[keyof ActionMap][];

export abstract class Resource {
export interface ResourceDetails<T> {
id: string;
provider: string;
service: string;
details: T;
}

export abstract class Resource<Detail = {}> {
/**
* Unique name for the resource by type within the stack.
*
Expand All @@ -47,6 +57,36 @@ export abstract class Resource {
this._registerPromise = promise;
}

/**
* Returns details of this
*/
protected async details(): Promise<ResourceDetails<Detail>> {
const req = new ResourceDetailsRequest();
const res = new ProtoResource();
res.setName(this.name);
res.setType(this.resourceType());

req.setResource(res);
return new Promise<ResourceDetails<Detail>>((resolve, reject) => {
resourceClient.details(req, (err, resp) => {
if (err) {
reject(fromGrpcError(err));
} else {
resolve({
id: resp.getId(),
provider: resp.getProvider(),
service: resp.getService(),
details: this.unwrapDetails(resp)
})
}
});
});
}

protected abstract resourceType(): ResourceTypeMap[keyof ResourceTypeMap];

protected abstract unwrapDetails(resp: ResourceDetailsResponse): Detail;

protected abstract register(): Promise<ProtoResource>;
}

Expand Down
9 changes: 9 additions & 0 deletions src/resources/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ResourceType,
Action,
ActionMap,
ResourceDeclareResponse,
} from '@nitric/api/proto/resource/v1/resource_pb';
import resourceClient from './client';
import { queues, Queue } from '../api/';
Expand Down Expand Up @@ -82,6 +83,14 @@ export class QueueResource extends SecureResource<QueuePermission> {
return actions;
}

protected resourceType() {
return ResourceType.QUEUE;
}

protected unwrapDetails(resp: ResourceDeclareResponse): {} {
throw new Error("details unimplemented for queue");
}

/**
* Return a queue reference and register the permissions required by the currently scoped function for this resource.
*
Expand Down
Loading

0 comments on commit b7c744b

Please sign in to comment.