diff --git a/.circleci/config.yml b/.circleci/config.yml index 6ecbd4d2..429fec01 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -222,6 +222,26 @@ jobs: npm install ../sdk/kalix-io-kalix-javascript-sdk-0.0.0.tgz npm test npm pack + - run: + name: "bundle quickstarts" + working_directory: "~/project/docs" + command: | + make bundles + - run: + name: "Test quickstart: JS Customer Registry" + working_directory: "~/tmp" + command: | + # use the bundled quickstart to check against what the user will download + unzip -d js-customer-registry ~/project/docs/build/src/managed/modules/javascript/attachments/js-customer-registry-quickstart.zip + cd js-customer-registry + export KALIX_NPMJS_CODEGEN_BINARY="${HOME}/project/codegen/js-gen-cli/target/native-image/kalix-codegen-js" + npm install --save \ + "$HOME/project/sdk/kalix-io-kalix-javascript-sdk-0.0.0.tgz" \ + "$HOME/project/npm-js/kalix-scripts/kalix-io-kalix-scripts-1.0.0.tgz" \ + "$HOME/project/testkit/kalix-io-testkit-0.0.0.tgz" + npm install + npm run build + npm test - run: name: "Test JS Value Entity Counter sample" command: | @@ -343,6 +363,29 @@ jobs: npm install ../sdk/kalix-io-kalix-javascript-sdk-0.0.0.tgz DEBUG='testcontainers*' npm run integration-test npm pack + - run: + name: "bundle quickstarts" + working_directory: "~/project/docs" + command: | + make bundles + - run: + name: "integration tests: js-customer-registry-quickstart" + working_directory: "~/tmp" + command: | + export VERSION_CHECK_ON_STARTUP=false + export KALIX_NPMJS_CODEGEN_BINARY="${HOME}/project/codegen/js-gen-cli/target/native-image/kalix-codegen-js" + source /opt/circleci/.nvm/nvm.sh + pushd ~/project/sdk && nvm install && popd + # use the bundled quickstart to check against what the user will download + unzip -d js-customer-registry ~/project/docs/build/src/managed/modules/javascript/attachments/js-customer-registry-quickstart.zip + cd js-customer-registry + npm install --save \ + "$HOME/project/sdk/kalix-io-kalix-javascript-sdk-0.0.0.tgz" \ + "$HOME/project/npm-js/kalix-scripts/kalix-io-kalix-scripts-1.0.0.tgz" \ + "$HOME/project/testkit/kalix-io-testkit-0.0.0.tgz" + npm install + npm run build + npm run integration-test - run: name: "integration tests: samples/js/js-customer-registry" command: | diff --git a/docs/Makefile b/docs/Makefile index 90b590b2..d083fa06 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -45,6 +45,7 @@ examples: mkdir -p "${managed_examples}" rsync -a --exclude-from=.examplesignore ../samples/js/js-doc-snippets/ "${managed_examples}/js-doc-snippets/" rsync -a --exclude-from=.examplesignore ../samples/js/js-customer-registry/ "${managed_examples}/js-customer-registry/" + rsync -a --exclude-from=.examplesignore ../samples/js/js-customer-registry-quickstart/ "${managed_examples}/js-customer-registry-quickstart/" rsync -a --exclude-from=.examplesignore ../samples/js/js-valueentity-shopping-cart/ "${managed_examples}/js-valueentity-shopping-cart/" rsync -a --exclude-from=.examplesignore ../samples/js/valueentity-counter/ "${managed_examples}/valueentity-counter/" rsync -a --exclude-from=.examplesignore ../samples/js/js-eventsourced-shopping-cart/ "${managed_examples}/js-eventsourced-shopping-cart/" @@ -56,7 +57,7 @@ examples: rsync -a --exclude-from=.examplesignore ../samples/ts/ts-replicated-entity-shopping-cart/ "${managed_examples}/ts-replicated-entity-shopping-cart/" bundles: - bin/bundle.sh --zip "${managed_attachments}/js-customer-registry-quickstart.zip" ../samples/js/js-customer-registry + bin/bundle.sh --zip "${managed_attachments}/js-customer-registry-quickstart.zip" ../samples/js/js-customer-registry-quickstart bin/bundle.sh --zip "${managed_attachments}/ts-customer-registry-quickstart.zip" ../samples/ts/ts-customer-registry bin/bundle.sh --zip "${managed_attachments}/js-eventsourced-shopping-cart.zip" ../samples/js/js-eventsourced-shopping-cart bin/bundle.sh --zip "${managed_attachments}/ts-eventsourced-shopping-cart.zip" ../samples/ts/ts-eventsourced-shopping-cart diff --git a/docs/config/validate-links.json b/docs/config/validate-links.json index f499a371..0cb02906 100644 --- a/docs/config/validate-links.json +++ b/docs/config/validate-links.json @@ -1,6 +1,6 @@ { "ignorePatterns": [ - { "pattern": "^https://mvnrepository\\.com" }, + { "pattern": "^http://127.0.0.1:8080" }, { "pattern": "^https://www.npmjs.com/org/kalix-io", "why": "npmjs having a bad day 2022-06-07" } ] } diff --git a/docs/dev/src/modules/ROOT/partials/include.adoc b/docs/dev/src/modules/ROOT/partials/include.adoc index 929c0a0f..c8244deb 100644 --- a/docs/dev/src/modules/ROOT/partials/include.adoc +++ b/docs/dev/src/modules/ROOT/partials/include.adoc @@ -1,3 +1,3 @@ -:minimum_docker_version: 19.03 +:minimum_docker_version: 20.10.8 :tab-icon: image:ROOT:new-tab.svg[width=12] diff --git a/docs/src/modules/javascript/pages/quickstart/cr-value-entity-javascript.adoc b/docs/src/modules/javascript/pages/quickstart/cr-value-entity-javascript.adoc new file mode 100644 index 00000000..474e40da --- /dev/null +++ b/docs/src/modules/javascript/pages/quickstart/cr-value-entity-javascript.adoc @@ -0,0 +1,243 @@ += Quickstart: Customer Registry in JavaScript + +include::ROOT:partial$include.adoc[] +include::javascript:partial$attributes.adoc[] + +Learn how to create a customer registry in JavaScript, package it into a container, and run it on Kalix. + +== Before you begin + +* If you're new to Kalix, https://console.kalix.io[create an account{tab-icon}, window="new"] so you can try it out for free. +* You'll also need to install the https://docs.kalix.io/kalix/install-kalix.html[Kalix CLI, window="new-doc"] to deploy from a terminal window. +* For this quickstart, you'll also need +** https://docs.docker.com/engine/install[Docker {minimum_docker_version} or higher, window="new"] +** https://nodejs.org/en/download/[Node.js {minimum_node_version}, window="new"] +** https://github.com/fullstorydev/grpcurl#installation[`grpcurl`, window="new"] + +[NOTE] +==== +If you want to bypass writing code and jump straight to the deployment: + +. Download the source code using the Kalix CLI: ++ +[source,command line] +---- +kalix quickstart download customer-registry-javascript +---- + +. Skip to <>. +==== + +== Writing the Customer Registry + +. From the command line, create a directory with the basic structure for your project using a template: ++ +[source,command line] +---- +npx @kalix-io/create-kalix-entity@latest customer-registry --template basic +---- + +. Change into the project directory: ++ +[source,command line] +---- +cd customer-registry +---- + +. Download and install project dependencies: ++ +[source,command line] +---- +npm install +---- + +== Define the external API + +The Customer Registry service will create or retrieve a customer, including their name, email, and mailing address. The `customer_api.proto` will contain the external API your clients will invoke. + +. Create a `proto` directory. ++ +[source,command line] +---- +mkdir proto +---- + +. Create a `customer_api.proto` file and save it in the `proto` directory. + +. Add declarations for: ++ +* The protobuf syntax version, `proto3`. +* The package name, `customer.api`. +* Import `google/protobuf/empty.proto` and Kalix `kalix/annotations.proto`. ++ +[source,proto,indent=0] +.proto/customer_api.proto +---- +include::javascript:example$js-customer-registry-quickstart/proto/customer_api.proto[tag=declarations] +---- + +. Add the service endpoint. The service endpoint is annotated with `kalix.codegen` indicating we want to generate a Value Entity for this service. ++ +[source,proto,indent=0] +.proto/customer_api.proto +---- +include::javascript:example$js-customer-registry-quickstart/proto/customer_api.proto[tag=service] +---- + +. Add messages to define the fields that comprise a `Customer` object (and its compound `Address`): ++ +[source,proto,indent=0] +.proto/customer_api.proto +---- +include::javascript:example$js-customer-registry-quickstart/proto/customer_api.proto[tag=messages] +---- + +. Add the message that will identify which customer to retrieve for the `GetCustomer` message: ++ +[source,proto,indent=0] +.proto/customer_api.proto +---- +include::javascript:example$js-customer-registry-quickstart/proto/customer_api.proto[tag=method-messages] +---- + +== Define the domain model + +The `customer_domain.proto` contains all the internal data objects (https://docs.kalix.io/reference/glossary.html#entity[Entities, window="new"]). The https://docs.kalix.io/reference/glossary.html#value_entity[Value Entity, window="new"] in this quickstart is a Key/Value store that stores only the latest updates. + +. Create a `customer_domain.proto` file and save it in the `proto` directory. + +. Add declarations for the proto syntax and domain package. ++ +[source,proto,indent=0] +.proto/customer_domain.proto +---- +include::javascript:example$js-customer-registry-quickstart/proto/customer_domain.proto[tag=declarations] +---- + +. Add the `CustomerState` message with fields for the customer data, and the `Address` message: ++ +[source,proto,indent=0] +.proto/customer_domain.proto +---- +include::javascript:example$js-customer-registry-quickstart/proto/customer_domain.proto[tag=domain] +---- + +. Run the `build` script from the project root directory to generate source classes, based on the protobuf definitions, in which you can add the business logic: ++ +[source,command line] +---- +npm run build +---- + +== Create command handlers + +Command handlers, as the name suggests, handle incoming requests before persisting them. + +. If it's not open already, open the generated `src/customer.js` file for editing. + +. Modify the `Create` handler by adding the logic to handle the command. The complete function should include the following: ++ +[source,javascript,indent=0] +.src/customer.js +---- +include::example$js-customer-registry-quickstart/src/customer.js[tag=create] +---- ++ +* The incoming message contains the request data from your client and the command handler updates the state of the customer. + +. Modify the `GetCustomer` handler as follows to handle the `GetCustomerRequest` command: ++ +[source, javascript, indent=0] +.src/customer.js +---- +include::example$js-customer-registry-quickstart/src/customer.js[tag=getCustomer] +---- ++ +* If that customer doesn't exist, processing the command fails. +* If the customer exists, the reply message contains the customer's information. +* The conversion between the domain CustomerState and the external API is straightforward, as they have the same fields. + +[NOTE] +==== +The `src/index.js` file already contains the required code to start your service and register it with Kalix. +==== + +== Package and deploy your service + +To build and publish the container image and then deploy the service, follow these steps: + +. If you haven't done so yet, sign in to your Kalix account. If this is your first time using Kalix, this will let you register an account, https://docs.kalix.io/projects/create-project.html[create your first project], and set this project as the default. ++ +[source,command line] +---- +kalix auth login +---- + +. Update the `config.dockerImage` setting in the `package.json` file with your container registry. + +. Use the `deploy` script to build the container image, publish it to the container registry as configured in the `package.json` file, and then automatically https://docs.kalix.io/services/deploy-service.html#_deploy[deploy the service] to Kalix using `kalix`: ++ +[source,command line] +---- +npm run deploy +---- + +. You can https://docs.kalix.io/services/deploy-service.html#_verify_service_status[verify the status of the deployed service] using: ++ +[source,command line] +---- +kalix service list +---- + +== Invoke your service + +Once the service has started successfully, you can https://docs.kalix.io/services/invoke-service.html#_testing_and_development[start a proxy locally] to access the service: + +[source,command line] +---- +kalix service proxy customer-registry --grpcui +---- + +The `--grpcui` option also starts and opens a https://docs.kalix.io/services/invoke-service.html#_using_the_built_in_graphical_client[gRPC web UI] for exploring and invoking the service (available at http://127.0.0.1:8080/ui/). + +Or you can use command line gRPC or HTTP clients, such as `grpcurl` or `curl`, to invoke the service through the proxy at `localhost:8080`, using plaintext connections. + +A customer can be created using the `Create` method on `CustomerService`, in the gRPC web UI, or with `grpcurl`: + +[source,command line] +---- +grpcurl \ + -d '{ + "customer_id": "abc123", + "email": "someone@example.com", + "name": "Someone", + "address": { + "street": "123 Some Street", + "city": "Somewhere" + } + }' \ + --plaintext localhost:8080 \ + customer.api.CustomerService/Create +---- + +The `GetCustomer` method can be used to retrieve this customer, in the gRPC web UI, or with `grpcurl`: + +[source,command line] +---- +grpcurl \ + -d '{"customer_id": "abc123"}' \ + --plaintext localhost:8080 \ + customer.api.CustomerService/GetCustomer +---- + +You can https://docs.kalix.io/services/invoke-service.html#_exposing_services_to_the_internet[expose the service to the internet]. A generated hostname will be returned from the expose command: + +[source,command line] +---- +kalix service expose customer-registry +---- + +== Next steps + +* You can learn more about xref:javascript:value-entity.adoc[Value Entities]. +* Do another https://docs.kalix.io/quickstart/sc-eventsourced-entity-javascript.html[Quickstart] to learn about Event Sourcing and xref:javascript:eventsourced.adoc[Event Sourced Entities]. diff --git a/samples/js/js-customer-registry-quickstart/.bundleignore b/samples/js/js-customer-registry-quickstart/.bundleignore new file mode 100644 index 00000000..8484ead5 --- /dev/null +++ b/samples/js/js-customer-registry-quickstart/.bundleignore @@ -0,0 +1,5 @@ +.gitignore +lib +node_modules +package-lock.json +user-function.desc diff --git a/samples/js/js-customer-registry-quickstart/.dockerignore b/samples/js/js-customer-registry-quickstart/.dockerignore new file mode 100644 index 00000000..6a020972 --- /dev/null +++ b/samples/js/js-customer-registry-quickstart/.dockerignore @@ -0,0 +1,5 @@ +/README.md +/docker-compose.yml +/lib/generated +/node_modules +/user-function.desc diff --git a/samples/js/js-customer-registry-quickstart/.gitignore b/samples/js/js-customer-registry-quickstart/.gitignore new file mode 100644 index 00000000..8ace6d7a --- /dev/null +++ b/samples/js/js-customer-registry-quickstart/.gitignore @@ -0,0 +1,5 @@ +/lib/generated +/node_modules +/user-function.desc +# test quickstarts without lock file +/package-lock.json diff --git a/samples/js/js-customer-registry-quickstart/Dockerfile b/samples/js/js-customer-registry-quickstart/Dockerfile new file mode 100644 index 00000000..ee69d121 --- /dev/null +++ b/samples/js/js-customer-registry-quickstart/Dockerfile @@ -0,0 +1,48 @@ +# This Dockerfile uses multi-stage build process. +# See https://docs.docker.com/develop/develop-images/multistage-build/ + +# Stage 1: Downloading dependencies and building the application +FROM node:14.19-buster-slim AS builder + +RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/* + +# Set the working directory +WORKDIR /home/node + +# Install app dependencies +COPY package*.json ./ +RUN npm ci + +# Copy sources and build the app +COPY --chown=node . . +RUN npm run build + +# Remove dev packages +# (the rest will be copied to the production image at stage 2) +RUN npm prune --production + +# Stage 2: Building the production image +FROM node:14.19-buster-slim + +# Set the working directory +WORKDIR /home/node + +# Copy dependencies +COPY --from=builder --chown=node /home/node/node_modules node_modules/ + +# Copy the app +COPY --from=builder --chown=node \ + /home/node/package*.json \ + /home/node/user-function.desc \ + ./ +COPY --from=builder --chown=node /home/node/proto ./proto +COPY --from=builder --chown=node /home/node/src ./src +COPY --from=builder --chown=node /home/node/lib ./lib + +# Run the app as an unprivileged user for extra security. +USER node + +# Run +EXPOSE 8080 +# Call node directly to get SIGTERM for graceful shutdown +CMD ["node", "src/index.js"] diff --git a/samples/js/js-customer-registry-quickstart/README.md b/samples/js/js-customer-registry-quickstart/README.md new file mode 100644 index 00000000..aaefa606 --- /dev/null +++ b/samples/js/js-customer-registry-quickstart/README.md @@ -0,0 +1,134 @@ +# Quickstart project: Customer Registry + + +## Designing + +To understand the Kalix concepts that are the basis for this example, see [designing +services](https://docs.kalix.io/services/development-process.html) in the documentation. + + +## Developing + +This project demonstrates the use of a Value Entity component to create a Customer Registry. + +To understand more about components, see [developing services](https://docs.kalix.io/services/) and +the [JavaScript section](https://docs.kalix.io/javascript/) in particular. + + +## Building + +You can use `npm` to build your project, which will also take care of generating code based on the +`.proto` definitions. First install dependencies: + +``` +npm install +``` + +Then run the `build` script: + +``` +npm run build +``` + + +## Testing + +Unit tests, that test against a mock entity, can be found in `test`. Run the unit tests with: + +``` +npm test +``` + +Integration tests, that make calls against a local service, can be found in `integration-test`. +Run the integration tests with: + +``` +npm run integration-test +``` + + +## Running Locally + +To run the example locally, you must run the Kalix proxy. The included `docker-compose` file +contains the configuration required to run the proxy for a locally running application. It also +contains the configuration to start a local Google Pub/Sub emulator that the Kalix proxy will +connect to. To start the proxy, run the following command from this directory: + +``` +docker-compose up +``` + +To start the application locally, use the following command: + +``` +npm start +``` + +With both the proxy and your application running, the defined endpoints should be available at +`http://localhost:9000`. In addition to the defined gRPC interface, each method has a corresponding +HTTP endpoint. Example calls using [grpcurl](https://github.com/fullstorydev/grpcurl): + +* Create a customer: + ``` + grpcurl \ + -d '{ + "customer_id": "abc123", + "email": "someone@example.com", + "name": "Someone", + "address": { + "street": "123 Some Street", + "city": "Somewhere" + } + }' \ + --plaintext localhost:9000 \ + customer.api.CustomerService/Create + ``` + +* Retrieve the customer: + ``` + grpcurl \ + -d '{"customer_id": "abc123"}' \ + --plaintext localhost:9000 \ + customer.api.CustomerService/GetCustomer + ``` + +* Change the customer's name: + ``` + grpcurl \ + -d '{ + "customer_id": "abc123", + "new_name": "New Someone" + }' \ + --plaintext localhost:9000 \ + customer.api.CustomerService/ChangeName + ``` + +* Change the customer's address: + ``` + grpcurl \ + -d '{ + "customer_id": "abc123", + "new_address": { + "street": "42 Some New Street", + "city": "New Somewhere" + } + }' \ + --plaintext localhost:9000 \ + customer.api.CustomerService/ChangeAddress + ``` + + +## Deploying + +To deploy your service, install the `kalix` CLI as documented in [setting up a local development +environment](https://docs.kalix.io/getting-started/set-up-development-env.html) and configure a +Docker Registry to upload your docker image to. + +You will need to update the `config.dockerImage` property in the `package.json`. Refer to +[configuring registries](https://docs.kalix.io/projects/container-registries.html) for more +information on how to make your docker image available to Kalix. + +Finally, you can use the [Kalix Console](https://console.kalix.io) to create a project and then +deploy your service into the project either by using `npm run deploy`, through the `kalix` CLI, or +via the web interface. When using `npm run deploy`, the deploy script will also conveniently package +and publish your docker image prior to deployment. diff --git a/samples/js/js-customer-registry-quickstart/docker-compose.yml b/samples/js/js-customer-registry-quickstart/docker-compose.yml new file mode 100644 index 00000000..365c3dbb --- /dev/null +++ b/samples/js/js-customer-registry-quickstart/docker-compose.yml @@ -0,0 +1,22 @@ +version: "3" +services: + kalix-proxy: + image: gcr.io/kalix-public/kalix-proxy:1.0.10 + command: -Dconfig.resource=dev-mode.conf -kalix.proxy.eventing.support=google-pubsub-emulator + ports: + - "9000:9000" + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + USER_FUNCTION_HOST: ${USER_FUNCTION_HOST:-host.docker.internal} + USER_FUNCTION_PORT: ${USER_FUNCTION_PORT:-8080} + PUBSUB_EMULATOR_HOST: gcloud-pubsub-emulator + # Uncomment to disable the JWT dev secret + # JWT_DEV_SECRET: "false" + # Uncomment to set the JWT dev secret issuer + # JWT_DEV_SECRET_ISSUER: "my-issuer" + gcloud-pubsub-emulator: + image: gcr.io/google.com/cloudsdktool/cloud-sdk:341.0.0 + command: gcloud beta emulators pubsub start --project=test --host-port=0.0.0.0:8085 + ports: + - 8085:8085 diff --git a/samples/js/js-customer-registry-quickstart/integration-test/customer.test.js b/samples/js/js-customer-registry-quickstart/integration-test/customer.test.js new file mode 100644 index 00000000..9d344b71 --- /dev/null +++ b/samples/js/js-customer-registry-quickstart/integration-test/customer.test.js @@ -0,0 +1,95 @@ +/* + * Copyright 2021 Lightbend Inc. + * + * 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 { IntegrationTestkit } from "@kalix-io/testkit"; +import { expect } from "chai"; +import customer from "../src/customer.js"; + +const testkit = new IntegrationTestkit(); +testkit.addComponent(customer); + +const client = () => testkit.clients.CustomerService; + +describe("CustomerService", function () { + this.timeout(60000); + + before((done) => testkit.start(done)); + after((done) => testkit.shutdown(done)); + + it("should create a new customer", async () => { + await client().createAsync({ + customerId: "abc123", + email: "foo@example.com", + name: "Foo Bar", + address: { street: "42 Something St", city: "Somewhere City" }, + }); + + { + const customer = await client().getCustomerAsync({ customerId: "abc123" }); + expect(customer.name).to.equal("Foo Bar"); + expect(customer.email).to.equal("foo@example.com"); + expect(customer.address).to.deep.equal({ street: "42 Something St", city: "Somewhere City" }); + } + }); + + it("should change the name of a customer", async () => { + await client().createAsync({ + customerId: "abc123", + email: "foo@example.com", + name: "Foo Bar", + address: { street: "42 Something St", city: "Somewhere City" }, + }); + + { + const customer = await client().getCustomerAsync({ customerId: "abc123" }); + expect(customer.name).to.equal("Foo Bar"); + } + + await client().changeNameAsync({ + customerId: "abc123", + newName: "Baz Qux", + }); + + { + const customer = await client().getCustomerAsync({ customerId: "abc123" }); + expect(customer.name).to.equal("Baz Qux"); + } + }); + + it("should change the address of a customer", async () => { + await client().createAsync({ + customerId: "abc123", + email: "foo@example.com", + name: "Foo Bar", + address: { street: "42 Something St", city: "Somewhere City" }, + }); + + { + const customer = await client().getCustomerAsync({ customerId: "abc123" }); + expect(customer.address).to.deep.equal({ street: "42 Something St", city: "Somewhere City" }); + } + + await client().changeAddressAsync({ + customerId: "abc123", + newAddress: { street: "123 Awesome Street", city: "New City" }, + }); + + { + const customer = await client().getCustomerAsync({ customerId: "abc123" }); + expect(customer.address).to.deep.equal({ street: "123 Awesome Street", city: "New City" }); + } + }); +}); diff --git a/samples/js/js-customer-registry-quickstart/jsconfig.json b/samples/js/js-customer-registry-quickstart/jsconfig.json new file mode 100644 index 00000000..292f36bc --- /dev/null +++ b/samples/js/js-customer-registry-quickstart/jsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "commonJS", + "target": "es6", + "checkJs": true + }, + "strict": true, + "exclude": ["lib/generated/proto.js"], + "include": ["src/**/*", "lib/**/*", "test/**/*"] +} diff --git a/samples/js/js-customer-registry-quickstart/package.json b/samples/js/js-customer-registry-quickstart/package.json new file mode 100644 index 00000000..206e7d9c --- /dev/null +++ b/samples/js/js-customer-registry-quickstart/package.json @@ -0,0 +1,35 @@ +{ + "name": "customer-registry", + "version": "0.0.1", + "type": "module", + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + }, + "dependencies": { + "@kalix-io/kalix-javascript-sdk": "^1.0.0-M10" + }, + "devDependencies": { + "@kalix-io/kalix-scripts": "^1.0.0-M10", + "@kalix-io/testkit": "^1.0.0-M10", + "chai": "^4.3.6", + "mocha": "^10.0.0" + }, + "config": { + "dockerImage": "my-docker-repo/customer-registry", + "sourceDir": "./src", + "testSourceDir": "./test", + "integrationTestSourceDir": "./integration-test", + "protoSourceDir": "./proto", + "generatedSourceDir": "./lib/generated", + "compileDescriptorArgs": [] + }, + "scripts": { + "start": "node src/index.js", + "test": "mocha ./test", + "integration-test": "mocha ./integration-test", + "build": "kalix-scripts build", + "package": "kalix-scripts package", + "deploy": "kalix-scripts deploy" + } +} diff --git a/samples/js/js-customer-registry-quickstart/proto/customer_api.proto b/samples/js/js-customer-registry-quickstart/proto/customer_api.proto new file mode 100644 index 00000000..c6b3b202 --- /dev/null +++ b/samples/js/js-customer-registry-quickstart/proto/customer_api.proto @@ -0,0 +1,74 @@ +// Copyright 2021 Lightbend Inc. +// +// 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. + +// tag::declarations[] +syntax = "proto3"; + +package customer.api; + +import "google/protobuf/empty.proto"; +import "kalix/annotations.proto"; +// end::declarations[] + +// tag::messages[] +message Customer { + string customer_id = 1 [(kalix.field).entity_key = true]; + string email = 2; + string name = 3; + Address address = 4; +} + +message Address { + string street = 1; + string city = 2; +} +// end::messages[] + +// tag::method-messages[] +message GetCustomerRequest { + string customer_id = 1 [(kalix.field).entity_key = true]; +} +// end::method-messages[] + +message ChangeNameRequest { + string customer_id = 1 [(kalix.field).entity_key = true]; + string new_name = 2; +} + +message ChangeAddressRequest { + string customer_id = 1 [(kalix.field).entity_key = true]; + Address new_address = 2; +} + +// tag::service[] +service CustomerService { + option (kalix.codegen) = { + value_entity: { + name: "customer.domain.Customer" + entity_type: "customers" + state: "customer.domain.CustomerState" + } + }; + // end::service[] + option (kalix.service).acl.allow = { principal: ALL }; + // tag::service[] + + rpc Create(Customer) returns (google.protobuf.Empty) {} + rpc GetCustomer(GetCustomerRequest) returns (Customer) {} + // end::service[] + rpc ChangeName(ChangeNameRequest) returns (google.protobuf.Empty) {} + rpc ChangeAddress(ChangeAddressRequest) returns (google.protobuf.Empty) {} + // tag::service[] +} +// end::service[] diff --git a/samples/js/js-customer-registry-quickstart/proto/customer_domain.proto b/samples/js/js-customer-registry-quickstart/proto/customer_domain.proto new file mode 100644 index 00000000..1bb22978 --- /dev/null +++ b/samples/js/js-customer-registry-quickstart/proto/customer_domain.proto @@ -0,0 +1,33 @@ +// Copyright 2021 Lightbend Inc. +// +// 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. + +// tag::declarations[] +syntax = "proto3"; + +package customer.domain; +// end::declarations[] + +// tag::domain[] +message CustomerState { + string customer_id = 1; + string email = 2; + string name = 3; + Address address = 4; +} + +message Address { + string street = 1; + string city = 2; +} +// end::domain[] diff --git a/samples/js/js-customer-registry-quickstart/proto/kalix_policy.proto b/samples/js/js-customer-registry-quickstart/proto/kalix_policy.proto new file mode 100644 index 00000000..d01e85bf --- /dev/null +++ b/samples/js/js-customer-registry-quickstart/proto/kalix_policy.proto @@ -0,0 +1,26 @@ +// Copyright 2021 Lightbend Inc. +// +// 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. + +// Default access control for all components of this Kalix Service + +syntax = "proto3"; + +package com.example; + +import "kalix/annotations.proto"; + +// only allow access from other services in the same project by default +option (kalix.file).acl = { + allow: { service: "*" } +}; diff --git a/samples/js/js-customer-registry-quickstart/src/customer.js b/samples/js/js-customer-registry-quickstart/src/customer.js new file mode 100644 index 00000000..f5428725 --- /dev/null +++ b/samples/js/js-customer-registry-quickstart/src/customer.js @@ -0,0 +1,91 @@ +/* + * Copyright 2021 Lightbend Inc. + * + * 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 { ValueEntity, Reply } from "@kalix-io/kalix-javascript-sdk"; + +/** + * Type definitions. + * These types have been generated based on your proto source. + * A TypeScript aware editor such as VS Code will be able to leverage them to provide hinting and validation. + * + * CustomerService; a strongly typed extension of ValueEntity derived from your proto source + * @typedef { import("../lib/generated/customer").CustomerService } CustomerService + */ + +/** + * @type CustomerService + */ +const entity = new ValueEntity( + [ + "customer_api.proto", + "customer_domain.proto", + "kalix_policy.proto", + ], + "customer.api.CustomerService", + "customers", + { + includeDirs: ["./proto"] + } +); + +const CustomerState = entity.lookupType("customer.domain.CustomerState"); + +entity.setInitial(entityId => CustomerState.create({})); + +entity.setCommandHandlers({ + // tag::create[] + Create(customer, _customerState, ctx) { + // API and domain messages have the same fields so conversion is easy + const customerState = CustomerState.create(customer); + ctx.updateState(customerState); + return Reply.message({}); + }, + // end::create[] + + // tag::getCustomer[] + GetCustomer(getCustomerRequest, customerState) { + if (!customerState.customerId) { + const id = getCustomerRequest.customerId; + return Reply.failure(`Customer ${id} has not been created.`); + } else { + // API and domain messages have the same fields so conversion is easy + return Reply.message(customerState); + } + }, + // end::getCustomer[] + + ChangeName(changeNameRequest, customerState, ctx) { + if (!customerState.name && !customerState.email) { + return Reply.failure("Customer must be created before name can be changed."); + } else { + customerState.name = changeNameRequest.newName; + ctx.updateState(customerState); + return Reply.message({}); + } + }, + + ChangeAddress(changeAddressRequest, customerState, ctx) { + if (!customerState.name) { + return Reply.failure("Customer must be created before address can be changed."); + } else { + customerState.address = changeAddressRequest.newAddress; + ctx.updateState(customerState); + return Reply.message({}); + } + }, +}); + +export default entity; diff --git a/samples/js/js-customer-registry-quickstart/src/index.js b/samples/js/js-customer-registry-quickstart/src/index.js new file mode 100644 index 00000000..84824efb --- /dev/null +++ b/samples/js/js-customer-registry-quickstart/src/index.js @@ -0,0 +1,29 @@ +/* + * Copyright 2021 Lightbend Inc. + * + * 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 { Kalix } from "@kalix-io/kalix-javascript-sdk"; +import generatedComponents from "../lib/generated/index.js"; + +const server = new Kalix(); + +// This generatedComponents array contains all generated Actions, Views or Entities, +// and is kept up-to-date with any changes in your protobuf definitions. +// If you prefer, you may remove this line and manually register these components. +generatedComponents.forEach((component) => { + server.addComponent(component); +}); + +server.start(); diff --git a/samples/js/js-customer-registry-quickstart/test/customer.test.js b/samples/js/js-customer-registry-quickstart/test/customer.test.js new file mode 100644 index 00000000..c8a462b8 --- /dev/null +++ b/samples/js/js-customer-registry-quickstart/test/customer.test.js @@ -0,0 +1,201 @@ +/* + * Copyright 2021 Lightbend Inc. + * + * 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 { MockValueEntity } from "@kalix-io/testkit"; +import { expect } from "chai"; +import customer from "../src/customer.js"; + +const CustomerState = customer.lookupType("customer.domain.CustomerState"); + +describe("CustomerService", () => { + const entityId = "entityId"; + + describe("Create", () => { + it("should create a new customer", async () => { + const entity = new MockValueEntity(customer, entityId); + + const response = await entity.handleCommand("Create", { + customerId: "abc123", + email: "foo@example.com", + name: "Foo Bar", + address: { street: "42 Something St", city: "Somewhere City" }, + }); + + expect(response).to.deep.equal({}); + expect(entity.error).to.be.undefined; + expect(entity.state).to.deep.equal( + CustomerState.create({ + customerId: "abc123", + email: "foo@example.com", + name: "Foo Bar", + address: { street: "42 Something St", city: "Somewhere City" }, + }), + ); + }); + }); + + describe("GetCustomer", () => { + it("should get an existing customer", async () => { + const entity = new MockValueEntity(customer, entityId); + + await entity.handleCommand("Create", { + customerId: "abc123", + email: "foo@example.com", + name: "Foo Bar", + address: { street: "42 Something St", city: "Somewhere City" }, + }); + + expect(entity.state).to.deep.equal( + CustomerState.create({ + customerId: "abc123", + email: "foo@example.com", + name: "Foo Bar", + address: { street: "42 Something St", city: "Somewhere City" }, + }), + ); + + const response = await entity.handleCommand("GetCustomer", { customerId: "abc123" }); + + expect(response).to.deep.equal({ + customerId: "abc123", + email: "foo@example.com", + name: "Foo Bar", + address: { street: "42 Something St", city: "Somewhere City" }, + }); + expect(entity.error).to.be.undefined; + expect(entity.state).to.deep.equal( + CustomerState.create({ + customerId: "abc123", + email: "foo@example.com", + name: "Foo Bar", + address: { street: "42 Something St", city: "Somewhere City" }, + }), + ); + }); + + it("should fail to get a non-existing customer", async () => { + const entity = new MockValueEntity(customer, entityId); + + const response = await entity.handleCommand("GetCustomer", { customerId: "abc123" }); + + expect(response).to.be.undefined; + expect(entity.error).to.be.equal("Customer abc123 has not been created."); + expect(entity.state).to.deep.equal(CustomerState.create({})); + }); + }); + + describe("ChangeName", () => { + it("should change the name of an existing customer", async () => { + const entity = new MockValueEntity(customer, entityId); + + await entity.handleCommand("Create", { + customerId: "abc123", + email: "foo@example.com", + name: "Foo Bar", + address: { street: "42 Something St", city: "Somewhere City" }, + }); + + expect(entity.state).to.deep.equal( + CustomerState.create({ + customerId: "abc123", + email: "foo@example.com", + name: "Foo Bar", + address: { street: "42 Something St", city: "Somewhere City" }, + }), + ); + + const response = await entity.handleCommand("ChangeName", { + customerId: "abc123", + newName: "Baz Qux", + }); + + expect(response).to.deep.equal({}); + expect(entity.error).to.be.undefined; + expect(entity.state).to.deep.equal( + CustomerState.create({ + customerId: "abc123", + email: "foo@example.com", + name: "Baz Qux", + address: { street: "42 Something St", city: "Somewhere City" }, + }), + ); + }); + + it("should fail to change the name of a non-existing customer", async () => { + const entity = new MockValueEntity(customer, entityId); + + const response = await entity.handleCommand("ChangeName", { + customerId: "abc123", + newName: "Baz Qux", + }); + + expect(response).to.be.undefined; + expect(entity.error).to.be.equal("Customer must be created before name can be changed."); + expect(entity.state).to.deep.equal(CustomerState.create({})); + }); + }); + + describe("ChangeAddress", () => { + it("should change the address of an existing customer", async () => { + const entity = new MockValueEntity(customer, entityId); + + await entity.handleCommand("Create", { + customerId: "abc123", + email: "foo@example.com", + name: "Foo Bar", + address: { street: "42 Something St", city: "Somewhere City" }, + }); + + expect(entity.state).to.deep.equal( + CustomerState.create({ + customerId: "abc123", + email: "foo@example.com", + name: "Foo Bar", + address: { street: "42 Something St", city: "Somewhere City" }, + }), + ); + + const response = await entity.handleCommand("ChangeAddress", { + customerId: "abc123", + newAddress: { street: "123 Awesome Street", city: "New City" }, + }); + + expect(response).to.deep.equal({}); + expect(entity.error).to.be.undefined; + expect(entity.state).to.deep.equal( + CustomerState.create({ + customerId: "abc123", + email: "foo@example.com", + name: "Foo Bar", + address: { street: "123 Awesome Street", city: "New City" }, + }), + ); + }); + + it("should fail to change the address of a non-existing customer", async () => { + const entity = new MockValueEntity(customer, entityId); + + const response = await entity.handleCommand("ChangeAddress", { + customerId: "abc123", + newAddress: { street: "123 Awesome Street", city: "New City" }, + }); + + expect(response).to.be.undefined; + expect(entity.error).to.be.equal("Customer must be created before address can be changed."); + expect(entity.state).to.deep.equal(CustomerState.create({})); + }); + }); +});