diff --git a/examples/documents/document-examples.test.ts b/examples/documents/document-examples.test.ts deleted file mode 100644 index 03a5e6f7..00000000 --- a/examples/documents/document-examples.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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 { - DocumentServiceClient -} from '@nitric/api/proto/document/v1/document_grpc_pb'; -import { - DocumentQueryResponse, - DocumentGetResponse, - DocumentSetResponse, - DocumentDeleteResponse, -} from '@nitric/api/proto/document/v1/document_pb'; -import { PassThrough } from 'stream'; -import { deleteDocument } from './delete'; -import { getDocument } from './get'; -import { queryDocument } from './query'; -import { queryFilterDocument } from './query-filter'; -import { queryLimitsDocument } from './query-limits'; -import { queryPaginatedDocument } from './query-paginated'; -import { queryStreamDocument } from './query-stream'; -import { getDocumentRef } from './refs'; -import { setDocument } from './set'; -import { querySubColQuery } from './sub-col-query'; -import { querySubDocQuery } from './sub-doc-query'; - -const docProto = DocumentServiceClient.prototype; - -const CALLBACKFN = (response) => (_, cb: any) => cb(null, response); - -describe('test document snippets', () => { - beforeAll(() => { - jest - .spyOn(docProto, 'get') - .mockImplementation(CALLBACKFN(new DocumentGetResponse())); - jest - .spyOn(docProto, 'set') - .mockImplementation(CALLBACKFN(new DocumentSetResponse())); - jest - .spyOn(docProto, 'delete') - .mockImplementation(CALLBACKFN(new DocumentDeleteResponse())); - jest - .spyOn(docProto, 'query') - .mockImplementation(CALLBACKFN(new DocumentQueryResponse())); - jest - .spyOn(docProto, 'queryStream') - // @ts-ignore - .mockReturnValueOnce(new PassThrough().end()); - }); - - test('ensure all document snippets run', async () => { - expect(getDocumentRef()).toEqual(undefined); - await expect(getDocument()).resolves.toEqual(null); - await expect(setDocument()).resolves.toEqual(undefined); - await expect(deleteDocument()).resolves.toEqual(undefined); - await expect(queryDocument()).resolves.toEqual(undefined); - await expect(queryFilterDocument()).resolves.toEqual(undefined); - await expect(queryLimitsDocument()).resolves.toEqual(undefined); - await expect(queryPaginatedDocument()).resolves.toEqual(undefined); - await expect(queryStreamDocument()).resolves.toEqual(undefined); - await expect(querySubDocQuery()).resolves.toEqual(undefined); - await expect(querySubColQuery()).resolves.toEqual(undefined); - }); -}); diff --git a/examples/documents/get.ts b/examples/documents/get.ts deleted file mode 100644 index d7b9f34b..00000000 --- a/examples/documents/get.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { documents } from '@nitric/sdk'; -// [END import] - -export async function getDocument() { - // [START snippet] - const documentRef = documents().collection('products').doc('nitric'); - - const product = await documentRef.get(); - // [END snippet] - return product; -} diff --git a/examples/documents/query-filter.ts b/examples/documents/query-filter.ts deleted file mode 100644 index caf2c04a..00000000 --- a/examples/documents/query-filter.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { documents } from '@nitric/sdk'; -// [END import] - -export async function queryFilterDocument() { - // [START snippet] - const docs = documents(); - - const query = docs - .collection('Customers') - .query() - .where('country', '==', 'US') - .where('age', '>=', 21); - - const results = await query.fetch(); - // [END snippet] -} diff --git a/examples/documents/query-limits.ts b/examples/documents/query-limits.ts deleted file mode 100644 index c240241e..00000000 --- a/examples/documents/query-limits.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { documents } from '@nitric/sdk'; -// [END import] - -export async function queryLimitsDocument() { - // [START snippet] - const docs = documents(); - - const query = docs.collection('Customers').query().limit(1000); - - const results = await query.fetch(); - // [END snippet] -} diff --git a/examples/documents/query-paginated.ts b/examples/documents/query-paginated.ts deleted file mode 100644 index 5820c5b8..00000000 --- a/examples/documents/query-paginated.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { documents } from '@nitric/sdk'; -// [END import] - -export async function queryPaginatedDocument() { - // [START snippet] - const docs = documents(); - - const query = docs - .collection('Customers') - .query() - .where('active', '==', true) - .limit(100); - - // Fetch first page - let results = await query.fetch(); - - // Fetch next page - if (results.pagingToken) { - results = await query.pagingFrom(results.pagingToken).fetch(); - } - // [END snippet] -} diff --git a/examples/documents/query-stream.ts b/examples/documents/query-stream.ts deleted file mode 100644 index 391c8820..00000000 --- a/examples/documents/query-stream.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { documents } from '@nitric/sdk'; -// [END import] - -export async function queryStreamDocument() { - // [START snippet] - const docs = documents(); - - const stream = docs.collection('Customers').query().stream(); - - for await (const doc of stream) { - // Process doc stream... - } - // [END snippet] -} diff --git a/examples/documents/query.ts b/examples/documents/query.ts deleted file mode 100644 index 53dbc353..00000000 --- a/examples/documents/query.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { documents } from '@nitric/sdk'; -// [END import] - -export async function queryDocument() { - // [START snippet] - const docs = documents(); - - const query = docs.collection('Customers').query(); - - // Execute query - const results = await query.fetch(); - // [END snippet] -} diff --git a/examples/documents/refs.ts b/examples/documents/refs.ts deleted file mode 100644 index 936f115f..00000000 --- a/examples/documents/refs.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { documents } from '@nitric/sdk'; -// [END import] - -export function getDocumentRef() { - // [START snippet] - // Create a reference to a collection named 'products' - const products = documents().collection('products'); - - // Create a reference to a document with the id 'nitric' - const nitric = products.doc('nitric'); - // [END snippet] -} diff --git a/examples/documents/set.ts b/examples/documents/set.ts deleted file mode 100644 index fe57ed20..00000000 --- a/examples/documents/set.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { documents } from '@nitric/sdk'; -// [END import] - -export async function setDocument() { - // [START snippet] - interface Product { - id: string; - name: string; - description: string; - } - - const docs = documents(); - - const document = docs.collection('products').doc('nitric'); - - await document.set({ - id: 'nitric', - name: 'nitric', - description: 'A development framework!', - }); - // [END snippet] -} diff --git a/examples/documents/sub-col-query.ts b/examples/documents/sub-col-query.ts deleted file mode 100644 index 6eb9171d..00000000 --- a/examples/documents/sub-col-query.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { documents } from '@nitric/sdk'; -// [END import] - -export async function querySubColQuery() { - // [START snippet] - const docs = documents(); - - const query = docs - .collection('Customers') - .collection('Order') - .query(); - - const results = await query.fetch(); - // [END snippet] -} diff --git a/examples/documents/sub-doc-query.ts b/examples/documents/sub-doc-query.ts deleted file mode 100644 index f529f090..00000000 --- a/examples/documents/sub-doc-query.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { documents } from '@nitric/sdk'; -// [END import] - -export async function querySubDocQuery() { - // [START snippet] - const docs = documents(); - - const query = docs - .collection('Customers') - .doc('apple') - .collection('Orders') - .query(); - - const results = await query.fetch(); - // [END snippet] -} diff --git a/examples/events/events-examples.test.ts b/examples/events/events-examples.test.ts deleted file mode 100644 index 0af7caab..00000000 --- a/examples/events/events-examples.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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 { EventServiceClient } from '@nitric/api/proto/event/v1/event_grpc_pb'; -import { EventPublishResponse } from '@nitric/api/proto/event/v1/event_pb'; - -import { eventsPublish } from './publish'; -import { eventsPublishIds } from './publish-ids'; - -const proto = EventServiceClient.prototype; - -const CALLBACKFN = (response) => (_, cb: any) => cb(null, response); - -describe('test events snippets', () => { - beforeAll(() => { - jest - .spyOn(proto, 'publish') - .mockImplementation(CALLBACKFN(new EventPublishResponse())); - }); - - test('ensure all events snippets run', async () => { - await expect(eventsPublish()).resolves.toEqual(undefined); - await expect(eventsPublishIds()).resolves.toEqual(undefined); - }); -}); diff --git a/examples/events/publish-ids.ts b/examples/events/publish-ids.ts deleted file mode 100644 index 25e0c3c6..00000000 --- a/examples/events/publish-ids.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { events } from '@nitric/sdk'; -// [END import] - -export async function eventsPublishIds() { - // [START snippet] - const topic = events().topic('my-topic'); - - const event = { - // Note: the event id should be generated using a process - // that's guaranteed to be unique for practical purposes. - id: 'unique-event-id', - payload: { - value: 'Hello World!', - }, - }; - - await topic.publish(event); - // [END snippet] -} diff --git a/examples/events/publish.ts b/examples/events/publish.ts deleted file mode 100644 index fe9e9b07..00000000 --- a/examples/events/publish.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { events } from '@nitric/sdk'; -// [END import] - -export async function eventsPublish() { - // [START snippet] - const topic = events().topic('my-topic'); - - const event = { - payloadType: 'my-payload', - payload: { - value: 'Hello World!', - }, - }; - - // Publish an event to the topic 'my-topic' - // Note: The event payload will be serialized automatically. - await topic.publish(event); - // [END snippet] -} diff --git a/examples/faas/events.ts b/examples/faas/events.ts deleted file mode 100644 index 5ac37138..00000000 --- a/examples/faas/events.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. - -// [START import] -import { faas } from "@nitric/sdk"; -// [END import] - -export const events = async () => { - // [START snippet] - await faas - .event(async (ctx) => { - console.log("received event: ", ctx.req.json()); - - // mark the event as successfully handled - ctx.res.success = true; - - return ctx; - }) - .start(); - // [END snippet] -} - diff --git a/examples/faas/faas-examples.test.ts b/examples/faas/faas-examples.test.ts deleted file mode 100644 index 8e25c810..00000000 --- a/examples/faas/faas-examples.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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 { events } from './events'; -import { FaasServiceClient } from '@nitric/api/proto/faas/v1/faas_grpc_pb'; - -const proto = FaasServiceClient.prototype; - -// We only need to handle half of the duplex stream -class MockClientStream { - public recievedMessages: Req[] = []; - - private listeners: { - [event: string]: ((req: Resp | string) => void)[]; - } = {}; - - public write(req: Req) { - this.recievedMessages.push(req); - } - - public on(event: string, cback: (req: Resp) => void) { - if (!this.listeners[event]) { - this.listeners[event] = []; - } - this.listeners[event].push(cback); - } - - public emit(event: string, req: Resp | string) { - if (this.listeners[event]) { - this.listeners[event].forEach((l) => l(req)); - } - } -} - -describe('test queues snippets', () => { - let mockStream = null as any; - - beforeEach(() => { - mockStream = new MockClientStream() as any; - jest.spyOn(proto, 'triggerStream').mockReturnValue(mockStream); - }); - - test('events snippet', async () => { - // Ensure event snippet is valid typescript - const evtPromise = events(); - - // close the stream - mockStream.emit('end', 'EOF'); - - await expect(evtPromise).resolves.toEqual(undefined); - }); -}); diff --git a/examples/queues/failed.ts b/examples/queues/failed.ts deleted file mode 100644 index 2362379f..00000000 --- a/examples/queues/failed.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { queues } from '@nitric/sdk'; -// [END import] - -export async function queueFailed(): Promise { - // [START snippet] - const taskList = [{ id: '1' }, { id: '2' }] - // Publish a collection of tasks - const failedMessages = await queues() - .queue('my-queue') - .send(taskList) - - // Check that it returned Failed Messages - for (const message in failedMessages) { - console.log(message) - } - // [END snippet] -} diff --git a/examples/queues/queues-examples.test.ts b/examples/queues/queues-examples.test.ts deleted file mode 100644 index 3f494006..00000000 --- a/examples/queues/queues-examples.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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 { queueReceive } from './receive'; -import { queueSend } from './send'; -import { QueueServiceClient } from '@nitric/api/proto/queue/v1/queue_grpc_pb'; -import { - QueueReceiveResponse, - QueueSendBatchResponse, -} from '@nitric/api/proto/queue/v1/queue_pb'; -import { queueFailed } from './failed'; - -const proto = QueueServiceClient.prototype; - -const CALLBACKFN = (response) => (_, cb: any) => cb(null, response); - -describe('test queues snippets', () => { - beforeAll(() => { - jest - .spyOn(proto, 'sendBatch') - .mockImplementation(CALLBACKFN(new QueueSendBatchResponse())); - jest - .spyOn(proto, 'receive') - .mockImplementation(CALLBACKFN(new QueueReceiveResponse())); - }); - - test('ensure all queues snippets run', async () => { - await expect(queueSend()).resolves.toEqual(undefined); - await expect(queueReceive()).resolves.toEqual(undefined); - await expect(queueFailed()).resolves.toEqual(undefined); - }); -}); diff --git a/examples/queues/receive.ts b/examples/queues/receive.ts deleted file mode 100644 index 2de4c2f0..00000000 --- a/examples/queues/receive.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { queues } from '@nitric/sdk'; -// [END import] - -export async function queueReceive() { - // [START snippet] - // Receive tasks from the queue - const tasks = await queues().queue('my-queue').receive(); - - await Promise.all( - tasks.map((task) => { - // Work on a task... - - // Complete the task - return task.complete(); - }) - ); - // [END snippet] -} diff --git a/examples/queues/send.ts b/examples/queues/send.ts deleted file mode 100644 index cfeb5779..00000000 --- a/examples/queues/send.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { queues } from '@nitric/sdk'; -// [END import] - -export async function queueSend(): Promise { - // [START snippet] - // Publish a task to the queue - const payload = { - example: 'payload', - }; - - await queues().queue('my-queue').send({ payload }); - // [END snippet] -} diff --git a/examples/queues/send_id.ts b/examples/queues/send_id.ts deleted file mode 100644 index 5e0ca78d..00000000 --- a/examples/queues/send_id.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { queues } from '@nitric/sdk'; -// [END import] - -export async function queueSend(): Promise { - // [START snippet] - // Publish a task to the queue - const payload = { - example: 'payload', - }; - - await queues().queue('my-queue').send({ id: 'unique-task-id', payload: payload }); - // [END snippet] -} diff --git a/examples/secrets/access.ts b/examples/secrets/access.ts deleted file mode 100644 index 55cf6e98..00000000 --- a/examples/secrets/access.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { secrets } from '@nitric/sdk'; -// [END import] - -export async function secretsAccess() { - // [START snippet] - // Access the latest secret - const value = await secrets().secret('database.password').latest().access(); - - const password = value.asString(); - // [END snippet] -} diff --git a/examples/secrets/put.ts b/examples/secrets/put.ts deleted file mode 100644 index 8d8716ce..00000000 --- a/examples/secrets/put.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { secrets } from '@nitric/sdk'; -// [END import] - -export async function secretsPut() { - // [START snippet] - const newPassword = 'qxGJp9rWMbYvPEsNFXzukQa!'; - - // Store the new password value, making it the latest version - const version = await secrets().secret('database.password').put(newPassword); - // [END snippet] -} diff --git a/examples/secrets/secrets-examples.test.ts b/examples/secrets/secrets-examples.test.ts deleted file mode 100644 index 2aa0e023..00000000 --- a/examples/secrets/secrets-examples.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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 { secretsAccess } from './access'; -import { secretsLatest } from './latest'; -import { secretsPut } from './put'; -import { SecretServiceClient } from '@nitric/api/proto/secret/v1/secret_grpc_pb'; -import { - SecretAccessResponse, - SecretPutResponse, - SecretVersion, - Secret -} from '@nitric/api/proto/secret/v1/secret_pb'; - -const proto = SecretServiceClient.prototype; - -describe('test secrets snippets', () => { - beforeAll(() => { - jest.spyOn(proto, 'access').mockImplementation((request, callback: any) => { - const mockResponse = new SecretAccessResponse(); - const s = new Secret(); - s.setName('database.password'); - const sv = new SecretVersion(); - sv.setSecret(s); - sv.setVersion('1'); - - mockResponse.setSecretVersion(sv); - const encoder = new TextEncoder(); - - mockResponse.setValue(encoder.encode('test')); - - callback(null, mockResponse); - - return null as any; - }); - - jest.spyOn(proto, 'put').mockImplementation((request, callback: any) => { - const mockResponse = new SecretPutResponse(); - const s = new Secret(); - s.setName('test'); - const sv = new SecretVersion(); - sv.setSecret(s); - sv.setVersion('1'); - - mockResponse.setSecretVersion(sv); - - callback(null, mockResponse); - - return null as any; - }); - }); - - test('ensure all secrets snippets run', async () => { - await expect(secretsPut()).resolves.toEqual(undefined); - await expect(secretsLatest()).resolves.toEqual(undefined); - await expect(secretsAccess()).resolves.toEqual(undefined); - }); -}); diff --git a/examples/storage/delete.ts b/examples/storage/delete.ts deleted file mode 100644 index 90426440..00000000 --- a/examples/storage/delete.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { storage } from '@nitric/sdk'; -// [END import] - -export async function storageDelete() { - // [START snippet] - // Construct a new storage client with default settings - const sc = storage(); - - // Delete a file from a bucket - await sc.bucket('my-bucket').file('path/to/item').delete(); - // [END snippet] -} diff --git a/examples/storage/read.ts b/examples/storage/read.ts deleted file mode 100644 index 661dfb93..00000000 --- a/examples/storage/read.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { storage } from '@nitric/sdk'; -// [END import] - -export async function storageRead() { - // [START snippet] - // Construct a new storage client with default settings - const sc = storage(); - - // Read a byte array from a bucket - const bytes = await sc.bucket('my-bucket').file('path/to/item').read(); - // [END snippet] -} diff --git a/examples/storage/signed-url-read.ts b/examples/storage/signed-url-read.ts deleted file mode 100644 index 93684340..00000000 --- a/examples/storage/signed-url-read.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { storage, FileMode } from '@nitric/sdk'; -// [END import] - -export async function storagePresignedUrlRead() { - // [START snippet] - // Construct a new storage client with default settings - const sc = storage(); - - // Get a signed url for reading a file - const url = await sc - .bucket('my-bucket') - .file('path/to/item') - .signUrl(FileMode.Read, { - // expiry in seconds - expiry: 3600, - }); - - return url; - // [END snippet] -} diff --git a/examples/storage/signed-url-write.ts b/examples/storage/signed-url-write.ts deleted file mode 100644 index 296d86f9..00000000 --- a/examples/storage/signed-url-write.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { storage, FileMode } from '@nitric/sdk'; -// [END import] - -export async function storagePresignedUrlWrite() { - // [START snippet] - // Construct a new storage client with default settings - const sc = storage(); - - // Get a signed url for for uploading file - const url = await sc - .bucket('my-bucket') - .file('path/to/item') - .signUrl(FileMode.Write, { - // expiry in seconds - expiry: 3600, - }); - - console.log('Generated PUT signed URL:'); - console.log(url); - console.log('You can use this URL with any user agent, for example:'); - console.log( - "curl -X PUT -H 'Content-Type: application/octet-stream' " + - `--upload-file my-file '${url}'` - ); - - return url; - // [END snippet] -} diff --git a/examples/storage/storage-examples.test.ts b/examples/storage/storage-examples.test.ts deleted file mode 100644 index 85516a16..00000000 --- a/examples/storage/storage-examples.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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 { StorageServiceClient } from '@nitric/api/proto/storage/v1/storage_grpc_pb'; -import { - StorageDeleteResponse, - StoragePreSignUrlResponse, - StorageReadResponse, - StorageWriteResponse, -} from '@nitric/api/proto/storage/v1/storage_pb'; - -import { storageDelete } from './delete'; -import { storageRead } from './read'; -import { storagePresignedUrlRead } from './signed-url-read'; -import { storagePresignedUrlWrite } from './signed-url-write'; -import { storageWrite } from './write'; - -const proto = StorageServiceClient.prototype; - -const CALLBACKFN = (response) => (_, cb: any) => cb(null, response); - -describe('test storage snippets', () => { - beforeAll(() => { - jest - .spyOn(proto, 'delete') - .mockImplementation(CALLBACKFN(new StorageDeleteResponse())); - jest - .spyOn(proto, 'read') - .mockImplementation(CALLBACKFN(new StorageReadResponse())); - jest - .spyOn(proto, 'write') - .mockImplementation(CALLBACKFN(new StorageWriteResponse())); - jest - .spyOn(proto, 'preSignUrl') - .mockImplementation(CALLBACKFN(new StoragePreSignUrlResponse())); - }); - - test('ensure all storage snippets run', async () => { - await expect(storageDelete()).resolves.toEqual(undefined); - await expect(storageRead()).resolves.toEqual(undefined); - await expect(storageWrite()).resolves.toEqual(undefined); - await expect(storagePresignedUrlRead()).resolves.toEqual(''); - await expect(storagePresignedUrlWrite()).resolves.toEqual(''); - }); -}); diff --git a/examples/storage/write.ts b/examples/storage/write.ts deleted file mode 100644 index e19297ae..00000000 --- a/examples/storage/write.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2021, Nitric Technologies Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// 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. -// [START import] -import { storage } from '@nitric/sdk'; -// [END import] - -export async function storageWrite() { - // [START snippet] - // Construct a new storage client with default settings - const sc = storage(); - - // Example byte array - const contents = new Uint8Array(); - - // Write a byte array to a bucket - await sc.bucket('my-bucket').file('path/to/item').write(contents); - // [END snippet] -} diff --git a/package.json b/package.json index 3a3d98c0..be5de870 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "license:header:check": "license-check-and-add check -f ./licenseconfig.json", "license:check": "licensee --production", "download:contracts": "curl -L https://github.com/nitrictech/nitric/releases/download/${npm_package_nitric}/contracts.tgz -o contracts.tgz && tar xvzf contracts.tgz && rm contracts.tgz", - "gen:proto": "yarn run download:contracts && mkdir -p ./src/gen && grpc_tools_node_protoc --ts_out=service=grpc-node,mode=grpc-js:./src/gen --js_out=import_style=commonjs,binary:./src/gen --grpc_out=grpc_js:./src/gen -I ./contracts ./contracts/**/*.proto ./contracts/proto/**/*/*.proto" + "gen:proto": "yarn run download:contracts && yarn run gen:sources", + "gen:sources": "mkdir -p ./src/gen && grpc_tools_node_protoc --ts_out=service=grpc-node,mode=grpc-js:./src/gen --js_out=import_style=commonjs,binary:./src/gen --grpc_out=grpc_js:./src/gen -I ./contracts ./contracts/**/*.proto ./contracts/proto/**/*/*.proto" }, "contributors": [ "Jye Cusch ", @@ -72,6 +73,7 @@ "ts-jest": "^26.4.3", "ts-node": "^10.9.1", "ts-protoc-gen": "^0.15.0", + "tsconfig-paths": "^4.2.0", "tsup": "^6.5.0", "typescript": "^4.4" }, diff --git a/examples/secrets/latest.ts b/src/api/websocket/index.ts similarity index 72% rename from examples/secrets/latest.ts rename to src/api/websocket/index.ts index 5635c7ac..1909a328 100644 --- a/examples/secrets/latest.ts +++ b/src/api/websocket/index.ts @@ -11,12 +11,3 @@ // 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. -// [START import] -import { secrets } from '@nitric/sdk'; -// [END import] - -export async function secretsLatest() { - // [START snippet] - const latestVersion = secrets().secret('database.password').latest(); - // [END snippet] -} diff --git a/examples/documents/delete.ts b/src/api/websocket/v0/index.ts similarity index 68% rename from examples/documents/delete.ts rename to src/api/websocket/v0/index.ts index 714e27b5..dc0a9182 100644 --- a/examples/documents/delete.ts +++ b/src/api/websocket/v0/index.ts @@ -11,16 +11,4 @@ // 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. -// [START import] -import { documents } from '@nitric/sdk'; -// [END import] - -export async function deleteDocument() { - // [START snippet] - const docs = documents(); - - const document = docs.collection('products').doc('nitric'); - - await document.delete(); - // [END snippet] -} +export * from './websocket'; diff --git a/src/api/websocket/v0/websocket.ts b/src/api/websocket/v0/websocket.ts new file mode 100644 index 00000000..2e68f25e --- /dev/null +++ b/src/api/websocket/v0/websocket.ts @@ -0,0 +1,101 @@ +// Copyright 2021, Nitric Technologies Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// 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 { SERVICE_BIND } from '../../../constants'; +import { WebsocketServiceClient } from '@nitric/api/proto/websocket/v1/websocket_grpc_pb'; +import { + WebsocketSendRequest, + WebsocketCloseRequest, +} from '@nitric/api/proto/websocket/v1/websocket_pb'; +import * as grpc from '@grpc/grpc-js'; +import { fromGrpcError } from '../../errors'; + +/** + * Nitric websocket client, facilitates sending messages to connections on this websocket. + */ +export class Websocket { + client: WebsocketServiceClient; + + constructor() { + this.client = new WebsocketServiceClient( + SERVICE_BIND, + grpc.ChannelCredentials.createInsecure() + ); + } + + async send( + socket: string, + connectionId: string, + message: string | Uint8Array | Record + ): Promise { + let payload: Uint8Array; + + // handle all message types + if (typeof message === 'string') { + payload = new TextEncoder().encode(message); + } else if (message instanceof Uint8Array) { + payload = message; + } else { + payload = new TextEncoder().encode(JSON.stringify(message)); + } + + const sendRequest = new WebsocketSendRequest(); + + sendRequest.setSocket(socket); + sendRequest.setConnectionId(connectionId); + sendRequest.setData(payload); + + return new Promise((res, rej) => { + this.client.send(sendRequest, (error, data) => { + if (error) { + rej(fromGrpcError(error)); + } + + res(); + }); + }); + } + + async close(socket: string, connectionId: string): Promise { + const closeRequest = new WebsocketCloseRequest(); + + closeRequest.setSocket(socket); + closeRequest.setConnectionId(connectionId); + + return new Promise((res, rej) => { + this.client.close(closeRequest, (error) => { + if (error) { + rej(fromGrpcError(error)); + } + + res(); + }); + }); + } +} + +// Websocket client singleton +let WEBSOCKET = undefined; + +/** + * Websocket API client. + * + * @returns a Websocket API client. + */ +export const websocket = (): Websocket => { + if (!WEBSOCKET) { + WEBSOCKET = new Websocket(); + } + + return WEBSOCKET; +}; diff --git a/src/faas/v0/context.ts b/src/faas/v0/context.ts index f59835b3..7ef161ff 100644 --- a/src/faas/v0/context.ts +++ b/src/faas/v0/context.ts @@ -20,6 +20,7 @@ import { NotificationResponseContext, TopicResponseContext, BucketNotificationType as ProtoBucketNotificationType, + WebsocketResponseContext, } from '@nitric/api/proto/faas/v1/faas_pb'; import * as api from '@opentelemetry/api'; import * as jspb from 'google-protobuf'; @@ -68,6 +69,15 @@ export abstract class TriggerContext< return undefined; } + /** + * Noop base context websocket method + * + * @returns undefined + */ + public get websocket(): WebsocketNotificationContext | undefined { + return undefined; + } + /** * Return the request object from this context. * @@ -107,6 +117,8 @@ export abstract class TriggerContext< trigger, options as BucketNotificationWorkerOptions ); + } else if (trigger.hasWebsocket()) { + return WebsocketNotificationContext.fromGrpcTriggerRequest(trigger); } throw new Error('Unsupported trigger request type'); } @@ -118,6 +130,8 @@ export abstract class TriggerContext< return EventContext.toGrpcTriggerResponse(ctx); } else if (ctx.bucketNotification) { return BucketNotificationContext.toGrpcTriggerResponse(ctx); + } else if (ctx.websocket) { + return WebsocketNotificationContext.toGrpcTriggerResponse(ctx); } throw new Error('Unsupported trigger context type'); @@ -595,3 +609,76 @@ export class FileNotificationRequest extends BucketNotificationRequest { export interface BucketNotificationResponse { success: boolean; } + +// WEBSOCKET NOTIFICATION CONTEXT + +export class WebsocketNotificationContext extends TriggerContext< + WebsocketNotificationRequest, + WebsocketNotificationResponse +> { + public get websocket(): WebsocketNotificationContext { + return this; + } + + static fromGrpcTriggerRequest( + trigger: TriggerRequest + ): WebsocketNotificationContext { + const ctx = new WebsocketNotificationContext(); + + ctx.request = new WebsocketNotificationRequest( + trigger.getData_asU8(), + getTraceContext(trigger.getTraceContext()), + trigger.getWebsocket().getSocket(), + trigger.getWebsocket().getEvent(), + trigger.getWebsocket().getConnectionid() + ); + + ctx.response = { + success: true, + }; + + return ctx; + } + + static toGrpcTriggerResponse( + ctx: TriggerContext + ): TriggerResponse { + const notifyCtx = ctx.websocket; + const triggerResponse = new TriggerResponse(); + const notificationResponse = new WebsocketResponseContext(); + notificationResponse.setSuccess(notifyCtx.res.success); + triggerResponse.setWebsocket(notificationResponse); + return triggerResponse; + } +} + +export enum WebsocketNotificationType { + Connected, + Disconnected, + Message, +} + +export class WebsocketNotificationRequest extends AbstractRequest { + public readonly socket: string; + public readonly notificationType: WebsocketNotificationType; + public readonly connectionId: string; + + constructor( + data: string | Uint8Array, + traceContext: api.Context, + socket: string, + notificationType: WebsocketNotificationType, + connectionId: string + ) { + super(data, traceContext); + + // Get reference to the bucket + this.socket = socket; + this.notificationType = notificationType; + this.connectionId = connectionId; + } +} + +export interface WebsocketNotificationResponse { + success: boolean; +} diff --git a/src/faas/v0/handler.ts b/src/faas/v0/handler.ts index 816dd3c1..a3b59cb2 100644 --- a/src/faas/v0/handler.ts +++ b/src/faas/v0/handler.ts @@ -18,6 +18,8 @@ import { EventContext, BucketNotificationContext, FileNotificationContext, + WebsocketNotificationContext, + JSONTypes, } from '.'; export type GenericHandler = (ctx: Ctx) => Promise | Ctx; @@ -35,6 +37,8 @@ export type GenericMiddleware = ( export type TriggerMiddleware = GenericMiddleware; export type HttpMiddleware = GenericMiddleware; +export type WebsocketMiddleware> = + GenericMiddleware>; export type EventMiddleware< T extends Record = Record > = GenericMiddleware>>; diff --git a/src/faas/v0/start.ts b/src/faas/v0/start.ts index cc51d423..28a3f75b 100644 --- a/src/faas/v0/start.ts +++ b/src/faas/v0/start.ts @@ -33,6 +33,9 @@ import { BucketNotificationWorker, BucketNotificationConfig, HttpWorker, + WebsocketResponseContext, + WebsocketWorker, + WebsocketEvent, } from '@nitric/api/proto/faas/v1/faas_pb'; import { @@ -45,6 +48,7 @@ import { TriggerContext, TriggerMiddleware, FileNotificationMiddleware, + WebsocketMiddleware, } from '.'; import newTracerProvider from './traceProvider'; @@ -59,6 +63,7 @@ import { import * as grpc from '@grpc/grpc-js'; import { HttpWorkerOptions } from '@nitric/sdk/resources/http'; +import { WebsocketWorkerOptions } from '@nitric/sdk/resources/websocket'; export class FaasWorkerOptions {} @@ -75,6 +80,7 @@ type FaasClientOptions = */ export class Faas { private httpHandler?: HttpMiddleware; + private websocketHandler?: WebsocketMiddleware; private eventHandler?: EventMiddleware | ScheduleMiddleware; private bucketNotificationHandler?: | BucketNotificationMiddleware @@ -108,6 +114,17 @@ export class Faas { return this; } + /** + * Add a websocket handler to this Faas server + * + * @param handlers the functions to call to respond to http requests + * @returns self + */ + websocket(...handlers: WebsocketMiddleware[]): Faas { + this.websocketHandler = createHandler(...handlers); + return this; + } + /** * Add a notification handler to this Faas server * @@ -152,6 +169,18 @@ export class Faas { return this.bucketNotificationHandler || this.anyHandler; } + /** + * Get websocket handler for this server + * + * @returns the registered websocket handler + */ + private getWebsocketHandler(): + | WebsocketMiddleware + | TriggerMiddleware + | undefined { + return this.websocketHandler || this.anyHandler; + } + /** * Start the Faas server * @@ -167,6 +196,7 @@ export class Faas { !this.httpHandler && !this.eventHandler && !this.bucketNotificationHandler && + !this.websocketHandler && !this.anyHandler ) { throw new Error('A handler function must be provided.'); @@ -222,6 +252,10 @@ export class Faas { triggerType = 'Notification'; handler = this.getBucketNotificationHandler() as GenericMiddleware; + } else if (ctx.websocket) { + triggerType = 'Websocket'; + handler = + this.getWebsocketHandler() as GenericMiddleware; } else { console.error( `received an unexpected trigger type, are you using an outdated version of the SDK?` @@ -266,6 +300,10 @@ export class Faas { const notificationResponse = new NotificationResponseContext(); notificationResponse.setSuccess(false); triggerResponse.setNotification(notificationResponse); + } else if (triggerRequest.hasWebsocket()) { + const notificationResponse = new WebsocketResponseContext(); + notificationResponse.setSuccess(false); + triggerResponse.setWebsocket(notificationResponse); } } // Send the response back to the membrane @@ -330,6 +368,11 @@ export class Faas { const httpWorker = new HttpWorker(); httpWorker.setPort(this.options.port); initRequest.setHttpWorker(httpWorker); + } else if (this.options instanceof WebsocketWorkerOptions) { + const websocketWorker = new WebsocketWorker(); + websocketWorker.setSocket(this.options.socket); + websocketWorker.setEvent(this.options.eventType); + initRequest.setWebsocket(websocketWorker); } // Original faas workers should return a blank InitRequest for compatibility. diff --git a/src/resources/http.ts b/src/resources/http.ts index 3bd90e9c..775f3a5a 100644 --- a/src/resources/http.ts +++ b/src/resources/http.ts @@ -23,15 +23,22 @@ interface NodeApplication { listen: ListenerFunction; } +// eslint-disable-next-line +const NO_OP = () => {}; + export class HttpWorkerOptions { public readonly app: NodeApplication; public readonly port: number; public readonly callback: () => void; - constructor(app: NodeApplication, port: number, callback?: () => void) { + constructor( + app: NodeApplication, + port: number, + callback: () => void = NO_OP + ) { this.app = app; this.port = port; - this.callback = callback || (() => {}); + this.callback = callback; } } diff --git a/src/resources/index.ts b/src/resources/index.ts index e46e8d0f..ab9df236 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -19,3 +19,4 @@ export * from './bucket'; export * from './schedule'; export * from './secret'; export * from './http'; +export * from './websocket'; diff --git a/src/resources/websocket.test.ts b/src/resources/websocket.test.ts new file mode 100644 index 00000000..933c2628 --- /dev/null +++ b/src/resources/websocket.test.ts @@ -0,0 +1,130 @@ +// Copyright 2021, Nitric Technologies Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// 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 { ResourceServiceClient } from '@nitric/api/proto/resource/v1/resource_grpc_pb'; +import { UnimplementedError } from '../api/errors'; +import { websocket } from '.'; +import { ResourceDeclareResponse } from '@nitric/api/proto/resource/v1/resource_pb'; +import * as faas from '../faas/index'; + +jest.mock('../faas/index'); + +describe('Registering websocket resources', () => { + describe('Given declare returns an error from the resource server', () => { + const MOCK_ERROR = { + code: 2, + message: 'UNIMPLEMENTED', + }; + + const validName = 'my-websocket'; + let declareSpy; + + beforeAll(() => { + declareSpy = jest + .spyOn(ResourceServiceClient.prototype, 'declare') + .mockImplementationOnce((request, callback: any) => { + callback(MOCK_ERROR, null); + + return null as any; + }); + }); + + afterAll(() => { + declareSpy.mockClear(); + }); + + it('Should throw the error', async () => { + await expect(websocket(validName)['registerPromise']).rejects.toEqual( + new UnimplementedError('UNIMPLEMENTED') + ); + }); + + it('Should call the resource server', () => { + expect(declareSpy).toBeCalledTimes(1); + }); + }); + + describe('Given declare succeeds on the resource server', () => { + describe('When the service succeeds', () => { + const validName = 'my-websocket2'; + let otherSpy; + + beforeAll(() => { + otherSpy = jest + .spyOn(ResourceServiceClient.prototype, 'declare') + .mockImplementation((request, callback: any) => { + const response = new ResourceDeclareResponse(); + callback(null, response); + return null as any; + }); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + it('Should succeed', async () => { + await expect( + websocket(validName)['registerPromise'] + ).resolves.not.toBeNull(); + }); + + it('Should call the resource server twice', () => { + expect(otherSpy).toBeCalledTimes(2); + }); + }); + }); + + describe('Given a topic is already registered', () => { + const websocketName = 'already-exists'; + let websocketResource; + let existsSpy; + + beforeEach(() => { + // ensure a success is returned and calls can be counted. + existsSpy = jest + .spyOn(ResourceServiceClient.prototype, 'declare') + .mockImplementation((request, callback: any) => { + const response = new ResourceDeclareResponse(); + callback(null, response); + return null as any; + }); + + // register the resource for the first time + websocketResource = websocket(websocketName); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('When registering a topic with the same name', () => { + let secondWebsocket; + + beforeEach(() => { + // make sure the initial registration isn't counted for these tests. + existsSpy.mockClear(); + secondWebsocket = websocket(websocketName); + }); + + it('Should not call the server again', () => { + expect(existsSpy).not.toBeCalled(); + }); + + it('Should return the same resource object', () => { + expect(websocketResource === secondWebsocket).toEqual(true); + }); + }); + }); +}); diff --git a/src/resources/websocket.ts b/src/resources/websocket.ts new file mode 100644 index 00000000..76113f56 --- /dev/null +++ b/src/resources/websocket.ts @@ -0,0 +1,182 @@ +// Copyright 2021, Nitric Technologies Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// 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 { fromGrpcError } from '../api/errors'; +import { Faas, JSONTypes, WebsocketMiddleware } from '../faas'; +import { + Websocket as WsClient, + websocket as wsClient, +} from '../api/websocket/v0'; +import { WebsocketEvent } from '../gen/proto/faas/v1/faas_pb'; +import { + Action, + PolicyResource, + Resource, + ResourceDeclareRequest, + ResourceDetailsResponse, + ResourceType, +} from '../gen/proto/resource/v1/resource_pb'; +import resourceClient from './client'; +import { make, Resource as Base } from './common'; + +const WebsocketEventTypeMap = { + connect: WebsocketEvent.CONNECT, + disconnect: WebsocketEvent.DISCONNECT, + message: WebsocketEvent.MESSAGE, +}; + +type WebsocketEventType = keyof typeof WebsocketEventTypeMap; + +export class WebsocketWorkerOptions { + public readonly socket: string; + public readonly eventType: (typeof WebsocketEventTypeMap)[WebsocketEventType]; + + constructor(socket: string, eventType: WebsocketEventType) { + this.socket = socket; + this.eventType = WebsocketEventTypeMap[eventType]; + } +} + +export class Websocket { + private readonly faas: Faas; + + constructor( + socket: string, + eventType: WebsocketEventType, + ...middleware: WebsocketMiddleware[] + ) { + this.faas = new Faas(new WebsocketWorkerOptions(socket, eventType)); + this.faas.websocket(...middleware); + } + + private async start(): Promise { + return this.faas.start(); + } +} + +/** + * Websocket resource for bi-di HTTP communication. + */ +export class WebsocketResource extends Base { + private readonly wsClient: WsClient; + + constructor(name: string) { + super(name); + this.wsClient = wsClient(); + } + + /** + * Register this websocket as a required resource for the calling function/container. + * + * @returns a promise that resolves when the registration is complete + */ + protected async register(): Promise { + const req = new ResourceDeclareRequest(); + const resource = new Resource(); + resource.setName(this.name); + resource.setType(ResourceType.WEBSOCKET); + + req.setResource(resource); + + const res = await new Promise((resolve, reject) => { + resourceClient.declare(req, (error, _: ResourceDeclareRequest) => { + if (error) { + reject(fromGrpcError(error)); + } else { + resolve(resource); + } + }); + }); + + const defaultPrincipal = new Resource(); + defaultPrincipal.setType(ResourceType.FUNCTION); + + const policyResource = new Resource(); + policyResource.setType(ResourceType.POLICY); + const policyReq = new ResourceDeclareRequest(); + const policy = new PolicyResource(); + policy.setActionsList([Action.WEBSOCKETMANAGE]); + policy.setPrincipalsList([defaultPrincipal]); + policy.setResourcesList([resource]); + policyReq.setPolicy(policy); + policyReq.setResource(policyResource); + + await new Promise((resolve, reject) => { + resourceClient.declare(policyReq, (error, _: ResourceDeclareRequest) => { + if (error) { + reject(fromGrpcError(error)); + } else { + resolve(resource); + } + }); + }); + + return res; + } + + async send( + connectionId: string, + // TODO: add less raw data types + data: string | Uint8Array | Record + ): Promise { + await this.wsClient.send(this.name, connectionId, data); + } + + async close(connectionId: string): Promise { + await this.wsClient.close(this.name, connectionId); + } + + /** + * Retrieves the Invocation URL of this Websocket at runtime. + * + * @returns Promise that returns the URL of this Websocket + */ + async url(): Promise { + const { + details: { url }, + } = await this.details(); + + return url; + } + + /** + * Register and start a websocket event handler that will be called for all matching events on this websocket + * + * @param eventType the notification type that should trigger the middleware, either 'connect', 'disconnect' or 'message' + * @param middleware handler middleware which will be run for every incoming event + * @returns Promise which resolves when the handler server terminates + */ + on>( + eventType: WebsocketEventType, + ...middleware: WebsocketMiddleware[] + ): Promise { + const notification = new Websocket(this.name, eventType, ...middleware); + return notification['start'](); + } + + protected resourceType() { + return ResourceType.WEBSOCKET; + } + + protected unwrapDetails(resp: ResourceDetailsResponse) { + if (resp.hasWebsocket()) { + return { + url: resp.getWebsocket().getUrl(), + }; + } + + throw new Error('Unexpected details in response. Expected API details'); + } +} + +export const websocket = make(WebsocketResource); diff --git a/yarn.lock b/yarn.lock index 3358cf90..d47c6b64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6348,6 +6348,15 @@ ts-protoc-gen@^0.15.0: dependencies: google-protobuf "^3.15.5" +tsconfig-paths@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"