diff --git a/examples/mongodb-credit-check-api/.gitignore b/examples/mongodb-credit-check-api/.gitignore new file mode 100644 index 0000000000..6a7d6d8ef6 --- /dev/null +++ b/examples/mongodb-credit-check-api/.gitignore @@ -0,0 +1,130 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* \ No newline at end of file diff --git a/examples/mongodb-credit-check-api/.prettierrc b/examples/mongodb-credit-check-api/.prettierrc new file mode 100644 index 0000000000..222861c341 --- /dev/null +++ b/examples/mongodb-credit-check-api/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} diff --git a/examples/mongodb-credit-check-api/README.md b/examples/mongodb-credit-check-api/README.md new file mode 100644 index 0000000000..9f481d6d4b --- /dev/null +++ b/examples/mongodb-credit-check-api/README.md @@ -0,0 +1,68 @@ +# Express Credit Check Workflow + +This is a simple workflow engine built with: + +- XState v5 +- TypeScript +- Express + +This is a modified version of the express-workflow project that shows how to implement state hydration in the `actorService.ts` file. +It also uses a more complex machine with guards, actions, and parallel states configured. + +**NOTE**: This project is _not_ production-ready and is intended for educational purposes. + +## Usage + +[MongoDB](https://www.mongodb.com/docs/manual/administration/install-community/) should be configured with a database named `creditCheck`. + +We recommend installing the [MongoDB Compass app](https://www.mongodb.com/products/tools/compass) to view the contents of your database while you run this project. + +Add the connection string to the DB client in the `actorService.ts` file by updating this line: + +```typescript +const uri = "/creditCheck"; +``` + +```bash +pnpm install +pnpm start +``` + +## Endpoints + +### POST `/workflows` + +Creates a new workflow instance. + +```bash +curl -X POST http://localhost:4242/workflows +``` + +Example response: +`201 - Created` + +```json +{ + {"message":"New worflow created successfully","workflowId":"uzkjyy"} +} +``` + +### POST `/workflows/:id` + +`200 - OK` + +Sends an event to a workflow instance. + +```bash +# Replace :id with the workflow ID; e.g. http://localhost:4242/workflows/7ky252 +# the body should be JSON +curl -X POST http://localhost:4242/workflows/:id -d '{"type": "Submit", "SSN": "123456789", "lastName": "Bauman", "firstName": "Gavin"}' -H "Content-Type: application/json" +``` + +### GET `/workflows/:id` + +Gets the current state of a workflow instance. + +```bash +curl -X GET http://localhost:4242/workflows/:id +``` diff --git a/examples/mongodb-credit-check-api/index.ts b/examples/mongodb-credit-check-api/index.ts new file mode 100644 index 0000000000..04a0569f7a --- /dev/null +++ b/examples/mongodb-credit-check-api/index.ts @@ -0,0 +1,103 @@ +import bodyParser from "body-parser"; +import { + collections, + getDurableActor, + initDbConnection, +} from "./services/actorService"; +import express from "express"; +import { creditCheckMachine } from "./machine"; + +const app = express(); + +app.use(bodyParser.json()); + +// Endpoint to start a new workflow instance +// - Generates a unique ID for the actor +// - Starts the actor +// - Persists the actor state +// - Returns a 201-Created code with the actor ID in the response +app.post("/workflows", async (_req, res) => { + console.log("starting new workflow..."); + try { + // Create a new actor and get its ID + const { workflowId } = await getDurableActor({ + machine: creditCheckMachine, + }); + res + .status(201) + .json({ message: "New worflow created successfully", workflowId }); + } catch (err) { + console.log(err); + res.status(500).send("Error starting workflow. Details: " + err); + } +}); + +// Endpoint to send events to an existing workflow instance +// - Gets the actor ID from request params +// - Gets the persisted state for that actor +// - Starts the actor with the persisted state +// - Sends the event from the request body to the actor +// - Persists the updated state +// - Returns the updated state in the response +app.post("/workflows/:workflowId", async (req, res) => { + const { workflowId } = req.params; + const event = req.body; + + try { + const { actor } = await getDurableActor({ + machine: creditCheckMachine, + workflowId, + }); + actor.send(event); + } catch (err) { + // note: you can (and should!) create custom errors to handle different scenarios and return different status codes + console.log(err); + res.status(500).send("Error sending event. Details: " + err); + } + + res + .status(200) + .send( + "Event received. Issue a GET request to see the current workflow state" + ); +}); + +// Endpoint to get the current state of an existing workflow instance +// - Gets the actor ID from request params +// - Gets the persisted state for that actor +// - Returns the persisted state in the response +app.get("/workflows/:workflowId", async (req, res) => { + const { workflowId } = req.params; + const persistedState = await collections.machineStates?.findOne({ + workflowId, + }); + + if (!persistedState) { + return res.status(404).send("Workflow state not found"); + } + + res.json(persistedState); +}); + +app.get("/", (_, res) => { + res.send(` + + +

Express Workflow

+

Start a new workflow instance:

+
curl -X POST http://localhost:4242/workflows
+

Send an event to a workflow instance:

+
curl -X POST http://localhost:4242/workflows/:workflowId -d '{"type":"TIMER"}'
+

Get the current state of a workflow instance:

+
curl -X GET http://localhost:4242/workflows/:workflowId
+ + + `); +}); + +// Connect to the DB and start the server +initDbConnection().then(() => { + app.listen(4242, () => { + console.log("Server listening on port 4242"); + }); +}); diff --git a/examples/mongodb-credit-check-api/machine.ts b/examples/mongodb-credit-check-api/machine.ts new file mode 100644 index 0000000000..8223d76178 --- /dev/null +++ b/examples/mongodb-credit-check-api/machine.ts @@ -0,0 +1,442 @@ +import { fromPromise, assign, setup } from "xstate"; +import { + checkBureauService, + checkReportsTable, + determineMiddleScore, + generateInterestRate, + saveCreditProfile, + saveCreditReport, + userCredential, + verifyCredentials, +} from "./services/machineLogicService"; +import CreditProfile from "./models/creditProfile"; + +export const creditCheckMachine = setup({ + types: { + events: {} as { + type: "Submit"; + SSN: string; + lastName: string; + firstName: string; + }, + context: {} as CreditProfile, + }, + + actors: { + checkBureau: fromPromise( + async ({ input }: { input: { ssn: string; bureauName: string } }) => + await checkBureauService(input) + ), + checkReportsTable: fromPromise( + async ({ input }: { input: { ssn: string; bureauName: string } }) => + await checkReportsTable(input) + ), + verifyCredentials: fromPromise( + async ({ input }: { input: userCredential }) => + await verifyCredentials(input) + ), + determineMiddleScore: fromPromise( + async ({ input }: { input: number[] }) => + await determineMiddleScore(input) + ), + generateInterestRates: fromPromise( + async ({ input }: { input: number }) => await generateInterestRate(input) + ), + }, + actions: { + saveReport: ( + { context }: { context: CreditProfile }, + params: { bureauName: string } + ) => { + console.log("saving report to the database..."); + saveCreditReport({ + ssn: context.SSN, + bureauName: params.bureauName, + creditScore: context.EquiGavinScore, + }); + }, + emailUser: function ({ context }) { + console.log( + "emailing user with their interest rate options: ", + context.InterestRateOptions + ); + }, + saveCreditProfile: async function ({ context }) { + console.log("saving results to the database..."); + await saveCreditProfile(context); + }, + emailSalesTeam: function ({ context, event }, params) { + console.log( + 'emailing sales team with the user"s information: ', + context.FirstName, + context.LastName, + context.InterestRateOptions, + context.MiddleScore + ); + }, + }, + guards: { + allSucceeded: ({ context }) => { + console.log("allSucceeded guard called"); + return ( + context.EquiGavinScore > 0 && + context.GavUnionScore > 0 && + context.GavperianScore > 0 + ); + }, + gavUnionReportFound: ({ context }) => { + return context.GavUnionScore > 0; + }, + equiGavinReportFound: ({ context }) => { + return context.EquiGavinScore > 0; + }, + gavperianReportFound: ({ context }) => { + return context.GavperianScore > 0; + }, + }, +}).createMachine({ + /** @xstate-layout N4IgpgJg5mDOIC5QFsCuAbALgSwA7rAGEAnSbTQgCzAGMBrAOhtInKtsYFEA7TMY7NygACAJLcAZgHtiyAIY4p3AMQBlVACNk5ANoAGALqJQuKbHLYlxkAA9EAZgAcAVgYB2ACwAmAIyO3zgA0IACeiD5ueu4AbM7RXs4AvonBaFh4BCRkFNT0TCxsuYwAavzYEiGCIlkQYLzYcuiwyhBKYAyCAG5SdO1pOPhEBTkc+dnseaUCFVXCNXU4jbAIXVI0Cpbc+gbb1qbmitzWdggAnB6u0Y72zqf2AcFhCAC0jgz20W5+AcmpGAOZYYTRjMcZFBhTcqVIRzFgLBpNZT8YgyBj4BTSWQMfoZIZg0ag1gjSZlGYw+b1JYrbjddaHba7JAgfYWKxMk56R6IPS-EA4wY1QoEoHg4FVQWYVQ0GRwFptBiwTAKPr-XES4FjIkasVCCVSmWwRkmMyso7sxBeDz+BgeU7OG56DwePTOZwuLkIa7RBiOFynPT2QNuaL2U6OXn8wH4vKEoV5HVQPXS0jNVrcdqK5XY1UCkXC6OMBNJg06HxGJksw7HRDOHyuV1uG5W+zeU5XD3PW7vRweOsfU5tl1eU4RnNRrXg2PEwtFcXDfUphgJzgAR1Q2AA4nJOoIl7OhAAxGScGzYRVVABKYFMxEwcvTHRpPXaYDXm+3ggAIgAhACCNEwGQjWZE0q3NBAAlOd5-ViV1HS8LxfQ9HxELeFDLRuAdh1ONxR3SXMC01OMZw4OdsgXOA91IoRV3XLcd24Kj6CqI9iBPM8cCEK8bzvNN2lWXoGFfOiP24H9-0A4hS3LY0Dk2asIK7UM9Fg5x4NQ5CnSieIMNuU5sNwlI+THPEJ3zMz433RN52TSjlzfejdwTVj2PPLjrxkO9kVRdFMExZAhIc0TxIAoDDD2UD5PAyDoJU101O8DTQnCN17HeNxdKwgdDL+fDx2IojpyYugyKJCjYGKqpaPfBiGAPMBMBoShLw829734p9BOEmrBHqxrKAksKZJAuS2VAE4fCtKDPjdHwUMiBJJs0vQvAYe1nC8Nw3EcaIZuiDw8IBUyCqnbUrOLRd7JE2q+qalqeKRYgUWINF0AxGQAu6xzuFugbQqk8KK0isbbHCW0ohbewfCh2s5r0O1NNdNaPltHtHEtbxDrVPMYxxkjmN1GyDUqoR6IAVW4TYSagFzTzcqBuM89rH26QSoG3CnNhCyTgMrKLxsQAJvUiOJfT0Rw9BW3bkO2nx3hbW5+y8PRIhy4y8uOorTtFc6icuqzycppRqdpjj7qZviWefBh2c6TmlG5oCywi0azQFxThZdK4JYlqXomQu40uuC47mifSVZVrGCIskE8epi67INjmjcY5zjzpziGdarynp8t6-I+m3k65v9-t54G3dBj33C9sXfa8aXkoQZXgzWntFbDluXSj-Ktbjos9cT6ioENqnfvNtrLYE9pbftn6GqawaAeGvmQYmjxYgYBJlLiHxznOf2m78AcGD37bPDbXs5pHIzI01jVtdGAfyNsiqE1H43x-ch7vJe3z-KLnbFOv0l7l1dgpSam99LoTdNEAMG03AB1tO4VWalHCnD8PYS0Pd76Tn7rrF+xN37blwGUOQqcrKm3pozSe8pp6ANIQIchwJQGA1kqaBS20oiug3v4eG4tQwem2q4S0-hMG+nQc4NWd91R4MIs-Mqr9qb0UYQ0Chw8qGZxobxOhnUZ4kLIdwFhZdnZA3AdFCWa1nC8MiP6a4pwPR3DcO4K0XxrgSLtNIkysjzIFQUeQcqyiDFMPUQTGm6czbfyZr-V670sS21UcwoorCV4VwgXFNaXgnBOhmr2QMyE6zOPiNECI-oAjeGhjgnxuN5EEMUUQpOnREmMS-lnB6U89FMCKPRAACoYlJLsOHgTrI6U+bg2x8LsYIpu-oPA2jDG4pwvpPFVLjo-Syw8E5v0ac0uqC9mpRLajE-+hcmocF6f0subCRpDPdiMuZpSri2IEQ4mZtYGADjEe45ZUjVmEXWYwT8DV+DaEpkIcQfAUyYAvMqAA8rgQ4FUgWQtBVUAAstgCAEACAUWZvQ6ezwNAQAAFboGQGA25VcsGfAYGHXaiE9BzSuIfJ4O1XCSycbA-wO1oh-JjoVDUyKQWCCqBC-gcBoVwoRZsCqrSYV8FTLo1mKoNbVNjoRIVsgRXgt4OKxU8qwDwsRXs-ql5lTLFWHSTYDJrmr0rice4e9aURBcC2HCGMPS+mcY2DwgYslSIQjcZIRluBSFqPAJkMi8aDLAu7Z4bYOxZLmdkvsYdBz2j5SdaNZjKUnGeFaDskCfQ9lTQOOBGbb7eLWXHHgkLZjiH8hsEGdrOFpRDIhFCQQj5dniEkStqrq2EUhGSaocJKRNBjfzKuJTvX2hgpNHw8CPAehuM450torQR0DLaTNfdambMHhG9hsaq4dvlk4JNYdbj+o9NY70ojFkeN+f2o6aqBU6wPYQ-Ww9qrfUnWvBww5z3oxbFe-SNxkLK20ghX1ekDK7ofvgz99Tv1hN-aJE2ETqHZ3-fahwW1gOXrtOBrtTw+zOPQRlOsVoULroQ3I-l-jJRKKuj1Fp+zxRSGQIMPguGIEZTeIy-1UyVI+GQjcdlK04hQwQjtH4L7sb-KQ2ErZ1N0M3Q44czAfHhnQ2mh8Tw-hrEZQRkfXsrh7gJUwQkBu9HfFFSY4E1j30TV3UPHIbABAIA6buV8Vays-C0Y7vYZC1i0pzRcMODKXx9p9tyq+wdjG6kBJY40uePmqX2CiMGRwfgfaSwbnEZCYcg4K1DuHVWdmalJeQylhpw8P6hJKoeLDWicM5pPQ67aMRct1wK7EFlFoAyrRUl8RsKs4ghiq+qmrKnD1BKAWPTTiYuM8bABliaXwoh3AbH58Zm1kLXDSkOFCLYVJhydNN99T9kvMfq2ExrrmDltM8ht8I18bROAypLe4+0LgB3Fm3L4A57R1hB1dgF8d5vEMW5-ZbB4PNebewgCIKEt6Mty5aDuiEA4EZuC2H1wYMEoQh8p5r1kv1Dwe8EtRyPAxpUtDBiWWV9ohabmHb1rjxE-K8QOpT+65uU+2Q1mn5DMNsQzhPbTHWp0Oty1vRKm6We+uQo6KIto71OiwVDSpCno5ZoF+T1TMPdmtMIKtggvGZcAZR54IOysZP8PsZ6kMCuvlLMkbzhL-PZtG+hzswxT2pfI7msgip-gQ4vMcdDN3j6eek8N6VOrqHycqMD60hHnnIAh6vafBIAYXTqSQkfAMUEH3c8kQdPXvdEMauBVqsFUAxVQoNUamVyPEJxFpXEDBekMpOg9Bdj5PY-WrsDXF9W3v+WQ81ainVkKJWt+lUoJF9e59QAxVinFtkO+zO72HMH7qB9NxcFEJNo+A3+oT-y2f2qm+6pb1K41crzV04uOF3sw54ZH+XSf135-tdL8g1q9cF7NBU1879m9F8n8ZUGADVYAekUQdxahvNrc8MEBQxXdO1do+8PUT8rRh9fVACMor9g0gA */ + context: { + SSN: "", + FirstName: "", + LastName: "", + GavUnionScore: 0, + EquiGavinScore: 0, + GavperianScore: 0, + ErrorMessage: "", + MiddleScore: 0, + InterestRateOptions: [], + }, + id: "multipleCreditCheck", + initial: "creditCheck", + states: { + creditCheck: { + initial: "Entering Information", + states: { + "Entering Information": { + on: { + Submit: { + target: "Verifying Credentials", + reenter: true, + }, + }, + }, + + "Verifying Credentials": { + invoke: { + input: ({ event }) => event, + src: "verifyCredentials", + onDone: { + target: "CheckingCreditScores", + actions: assign({ + SSN: ({ event }) => event.output.SSN, + FirstName: ({ event }) => event.output.firstName, + LastName: ({ event }) => event.output.lastName, + }), + }, + onError: [ + { + target: "Entering Information", + actions: assign({ + ErrorMessage: ({ + event, + }: { + context: any; + event: { error: any }; + }) => "Failed to verify credentials. Details: " + event.error, + }), + }, + ], + }, + }, + + CheckingCreditScores: { + description: + "Kick off a series of requests to the 3 American Credit Bureaus and await their results", + states: { + CheckingEquiGavin: { + initial: "CheckingForExistingReport", + states: { + CheckingForExistingReport: { + invoke: { + input: ({ context: { SSN } }) => ({ + bureauName: "EquiGavin", + ssn: SSN, + }), + src: "checkReportsTable", + id: "equiGavinDBActor", + onDone: [ + { + actions: assign({ + EquiGavinScore: ({ event }) => + event.output?.creditScore ?? 0, + }), + target: "FetchingComplete", + guard: "equiGavinReportFound", + }, + { + target: "FetchingReport", + }, + ], + onError: [ + { + target: "FetchingFailed", + }, + ], + }, + }, + FetchingComplete: { + type: "final", + entry: [ + { + type: "saveReport", + params: { + bureauName: "EquiGavin", + }, + }, + ], + }, + FetchingReport: { + invoke: { + input: ({ context: { SSN } }) => ({ + bureauName: "EquiGavin", + ssn: SSN, + }), + src: "checkBureau", + id: "equiGavinFetchActor", + onDone: [ + { + actions: assign({ + EquiGavinScore: ({ event }) => event.output ?? 0, + }), + target: "FetchingComplete", + }, + ], + onError: [ + { + target: "FetchingFailed", + }, + ], + }, + }, + FetchingFailed: { + type: "final", + }, + }, + }, + CheckingGavUnion: { + initial: "CheckingForExistingReport", + states: { + CheckingForExistingReport: { + invoke: { + input: ({ context: { SSN } }) => ({ + bureauName: "GavUnion", + ssn: SSN, + }), + src: "checkReportsTable", + id: "gavUnionDBActor", + onDone: [ + { + actions: assign({ + GavUnionScore: ({ event }) => + event.output?.creditScore ?? 0, + }), + target: "FetchingComplete", + guard: "gavUnionReportFound", + }, + { + target: "FetchingReport", + }, + ], + onError: [ + { + target: "FetchingFailed", + }, + ], + }, + }, + FetchingComplete: { + type: "final", + entry: [ + { + type: "saveReport", + params: { + bureauName: "GavUnion", + }, + }, + ], + }, + FetchingReport: { + invoke: { + input: ({ context: { SSN } }) => ({ + bureauName: "GavUnion", + ssn: SSN, + }), + src: "checkBureau", + id: "gavUnionFetchActor", + onDone: [ + { + actions: assign({ + GavUnionScore: ({ event }) => event.output ?? 0, + }), + target: "FetchingComplete", + }, + ], + onError: [ + { + target: "FetchingFailed", + }, + ], + }, + }, + FetchingFailed: { + type: "final", + }, + }, + }, + CheckingGavperian: { + initial: "CheckingForExistingReport", + states: { + CheckingForExistingReport: { + invoke: { + input: ({ context: { SSN } }) => ({ + bureauName: "Gavperian", + ssn: SSN, + }), + src: "checkReportsTable", + id: "gavperianCheckActor", + onDone: [ + { + actions: assign({ + GavperianScore: ({ event }) => + event.output?.creditScore ?? 0, + }), + target: "FetchingComplete", + guard: "gavperianReportFound", + }, + { + target: "FetchingReport", + }, + ], + onError: [ + { + target: "FetchingFailed", + }, + ], + }, + }, + FetchingComplete: { + type: "final", + entry: [ + { + type: "saveReport", + params: { + bureauName: "Gavperian", + }, + }, + ], + }, + FetchingReport: { + invoke: { + input: ({ context: { SSN } }) => ({ + ssn: SSN, + bureauName: "Gavperian", + }), + src: "checkBureau", + id: "checkGavPerianActor", + onDone: [ + { + actions: assign({ + GavperianScore: ({ event }) => event.output ?? 0, + }), + target: "FetchingComplete", + }, + ], + onError: [ + { + target: "FetchingFailed", + }, + ], + }, + }, + FetchingFailed: { + type: "final", + }, + }, + }, + }, + type: "parallel", + onDone: [ + { + target: "DeterminingInterestRateOptions", + guard: "allSucceeded", + reenter: true, + }, + { + target: "Entering Information", + actions: assign({ + ErrorMessage: ({ context }) => + "Failed to retrieve credit scores.", + }), + }, + ], + }, + + DeterminingInterestRateOptions: { + description: + "After retrieving results, determine the middle score to be used in home loan interest rate decision", + initial: "DeterminingMiddleScore", + states: { + DeterminingMiddleScore: { + invoke: { + input: ({ + context: { EquiGavinScore, GavUnionScore, GavperianScore }, + }) => [EquiGavinScore, GavUnionScore, GavperianScore], + src: "determineMiddleScore", + id: "scoreDeterminationActor", + onDone: [ + { + actions: [ + assign({ + MiddleScore: ({ event }) => event.output, + }), + { + type: "saveCreditProfile", + }, + ], + target: "FetchingRates", + }, + ], + }, + }, + FetchingRates: { + invoke: { + input: ({ context: { MiddleScore } }) => MiddleScore, + src: "generateInterestRates", + onDone: [ + { + actions: assign({ + InterestRateOptions: ({ event }) => [event.output], + }), + target: "RatesProvided", + }, + ], + }, + }, + RatesProvided: { + entry: [ + { + type: "emailUser", + }, + { + type: "emailSalesTeam", + }, + ], + type: "final", + }, + }, + }, + }, + }, + }, +}); diff --git a/examples/mongodb-credit-check-api/models/creditProfile.ts b/examples/mongodb-credit-check-api/models/creditProfile.ts new file mode 100644 index 0000000000..70da3f5f01 --- /dev/null +++ b/examples/mongodb-credit-check-api/models/creditProfile.ts @@ -0,0 +1,16 @@ +import { ObjectId } from "mongodb"; + +export default class CreditProfile { + constructor( + public SSN: string, + public LastName: string, + public FirstName: string, + public GavUnionScore: number, + public EquiGavinScore: number, + public GavperianScore: number, + public ErrorMessage: string, + public MiddleScore: number, + public InterestRateOptions: number[], + public _id?: ObjectId + ) {} +} diff --git a/examples/mongodb-credit-check-api/models/creditReport.ts b/examples/mongodb-credit-check-api/models/creditReport.ts new file mode 100644 index 0000000000..8864005051 --- /dev/null +++ b/examples/mongodb-credit-check-api/models/creditReport.ts @@ -0,0 +1,10 @@ +import { ObjectId } from "mongodb"; + +export default class CreditReport { + constructor( + public ssn: string, + public bureauName: string, + public creditScore: number, + public _id?: ObjectId + ) {} +} diff --git a/examples/mongodb-credit-check-api/package.json b/examples/mongodb-credit-check-api/package.json new file mode 100644 index 0000000000..76e2b33bac --- /dev/null +++ b/examples/mongodb-credit-check-api/package.json @@ -0,0 +1,27 @@ +{ + "name": "express-workflow", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "nodemon index.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@types/body-parser": "^1.19.5", + "body-parser": "^1.20.2", + "express": "^4.18.2", + "mongodb": "^6.3.0", + "xstate": "^5.0.0", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.9.0", + "nodemon": "^3.0.2", + "ts-node": "^10.9.1", + "typescript": "^5.2.2" + } +} diff --git a/examples/mongodb-credit-check-api/pnpm-lock.yaml b/examples/mongodb-credit-check-api/pnpm-lock.yaml new file mode 100644 index 0000000000..b321760d06 --- /dev/null +++ b/examples/mongodb-credit-check-api/pnpm-lock.yaml @@ -0,0 +1,1028 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@types/body-parser': + specifier: ^1.19.5 + version: 1.19.5 + body-parser: + specifier: ^1.20.2 + version: 1.20.2 + express: + specifier: ^4.18.2 + version: 4.18.2 + mongodb: + specifier: ^6.3.0 + version: 6.3.0 + xstate: + specifier: ^5.0.0 + version: 5.0.0 + zod: + specifier: ^3.22.4 + version: 3.22.4 + +devDependencies: + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/node': + specifier: ^20.9.0 + version: 20.9.0 + nodemon: + specifier: ^3.0.2 + version: 3.0.2 + ts-node: + specifier: ^10.9.1 + version: 10.9.1(@types/node@20.9.0)(typescript@5.2.2) + typescript: + specifier: ^5.2.2 + version: 5.2.2 + +packages: + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@mongodb-js/saslprep@1.1.4: + resolution: {integrity: sha512-8zJ8N1x51xo9hwPh6AWnKdLGEC5N3lDa6kms1YHmFBoRhTpJR6HG8wWk0td1MVCu9cD4YBrvjZEtd5Obw0Fbnw==} + dependencies: + sparse-bitfield: 3.0.3 + dev: false + + /@tsconfig/node10@1.0.9: + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + + /@types/body-parser@1.19.5: + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.9.0 + + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + dependencies: + '@types/node': 20.9.0 + + /@types/express-serve-static-core@4.17.41: + resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==} + dependencies: + '@types/node': 20.9.0 + '@types/qs': 6.9.10 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + dev: true + + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.17.41 + '@types/qs': 6.9.10 + '@types/serve-static': 1.15.5 + dev: true + + /@types/http-errors@2.0.4: + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + dev: true + + /@types/mime@1.3.5: + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + dev: true + + /@types/mime@3.0.4: + resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} + dev: true + + /@types/node@20.9.0: + resolution: {integrity: sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==} + dependencies: + undici-types: 5.26.5 + + /@types/qs@6.9.10: + resolution: {integrity: sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==} + dev: true + + /@types/range-parser@1.2.7: + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + dev: true + + /@types/send@0.17.4: + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.9.0 + dev: true + + /@types/serve-static@1.15.5: + resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==} + dependencies: + '@types/http-errors': 2.0.4 + '@types/mime': 3.0.4 + '@types/node': 20.9.0 + dev: true + + /@types/webidl-conversions@7.0.3: + resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + dev: false + + /@types/whatwg-url@11.0.4: + resolution: {integrity: sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==} + dependencies: + '@types/webidl-conversions': 7.0.3 + dev: false + + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: true + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + + /acorn-walk@8.3.0: + resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@8.11.2: + resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: false + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /body-parser@1.20.2: + resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /bson@6.2.0: + resolution: {integrity: sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==} + engines: {node: '>=16.20.1'} + dev: false + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + set-function-length: 1.1.1 + dev: false + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + dev: false + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: false + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + + /debug@4.3.4(supports-color@5.5.0): + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 5.5.0 + dev: true + + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: false + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: false + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: false + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + + /express@4.18.2: + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: false + + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + dependencies: + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + dev: false + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.2 + dev: false + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + dependencies: + get-intrinsic: 1.2.2 + dev: false + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + + /memory-pager@1.5.0: + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + dev: false + + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + dev: false + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /mongodb-connection-string-url@3.0.0: + resolution: {integrity: sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==} + dependencies: + '@types/whatwg-url': 11.0.4 + whatwg-url: 13.0.0 + dev: false + + /mongodb@6.3.0: + resolution: {integrity: sha512-tt0KuGjGtLUhLoU263+xvQmPHEGTw5LbcNC73EoFRYgSHwZt5tsoJC110hDyO1kjQzpgNrpdcSza9PknWN4LrA==} + engines: {node: '>=16.20.1'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.188.0 + '@mongodb-js/zstd': ^1.1.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: '>=6.0.0 <7' + snappy: ^7.2.2 + socks: ^2.7.1 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + dependencies: + '@mongodb-js/saslprep': 1.1.4 + bson: 6.2.0 + mongodb-connection-string-url: 3.0.0 + dev: false + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + + /nodemon@3.0.2: + resolution: {integrity: sha512-9qIN2LNTrEzpOPBaWHTm4Asy1LxXLSickZStAQ4IZe7zsoIpD/A7LWxhZV3t4Zu352uBcqVnRsDXSMR2Sc3lTA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + chokidar: 3.5.3 + debug: 4.3.4(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.5.4 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.0 + undefsafe: 2.0.5 + dev: true + + /nopt@1.0.10: + resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: false + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + + /path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + dev: false + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + dev: true + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: false + + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: false + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + dev: false + + /set-function-length@1.1.1: + resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: false + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + object-inspect: 1.13.1 + dev: false + + /simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + dev: true + + /sparse-bitfield@3.0.3: + resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + dependencies: + memory-pager: 1.5.0 + dev: false + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + + /touch@3.1.0: + resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} + hasBin: true + dependencies: + nopt: 1.0.10 + dev: true + + /tr46@4.1.1: + resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} + engines: {node: '>=14'} + dependencies: + punycode: 2.3.1 + dev: false + + /ts-node@10.9.1(@types/node@20.9.0)(typescript@5.2.2): + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.9.0 + acorn: 8.11.2 + acorn-walk: 8.3.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.2.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false + + /typescript@5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: false + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false + + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: false + + /whatwg-url@13.0.0: + resolution: {integrity: sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==} + engines: {node: '>=16'} + dependencies: + tr46: 4.1.1 + webidl-conversions: 7.0.0 + dev: false + + /xstate@5.0.0: + resolution: {integrity: sha512-FIKtRv8eQB+3dRLdnzdxlQ/sRjcqiN31JaskeHbcyRBaEvcwcjbakYZ5PVV9v4K0szbe793SUN/04J6/g5GrEA==} + dev: false + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true + + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: false diff --git a/examples/mongodb-credit-check-api/services/actorService.ts b/examples/mongodb-credit-check-api/services/actorService.ts new file mode 100644 index 0000000000..41ae156a43 --- /dev/null +++ b/examples/mongodb-credit-check-api/services/actorService.ts @@ -0,0 +1,102 @@ +import * as mongoDB from "mongodb"; +import { AnyStateMachine, createActor } from "xstate"; + +// mongoDB collections +export const collections: { + machineStates?: mongoDB.Collection; + creditReports?: mongoDB.Collection; + creditProfiles?: mongoDB.Collection; +} = {}; + +// Initialize DB Connection and Credit Check Actor +export async function initDbConnection() { + try { + //example uri + //const uri = "mongodb://localhost:27017/creditCheck"; + const uri = "/creditCheck"; + const client = new mongoDB.MongoClient(uri, { + serverApi: mongoDB.ServerApiVersion.v1, + }); + const db = client.db("creditCheck"); + collections.machineStates = db.collection("machineStates"); + await client.connect(); + } catch (err) { + console.log("Error connecting to the db...", err); + throw err; + } +} + +// create an actor to be used in the API endpoints +// hydrate the actor if a workflowId is provided +// otherwise, create a new workflowId +// persist the actor state to the db +export async function getDurableActor({ + machine, + workflowId, +}: { + machine: AnyStateMachine; + workflowId?: string; +}) { + let restoredState; + // if workflowId is provided, hydrate the actor with the persisted state from the db + // otherwise, just create a new workflowId + if (workflowId) { + restoredState = await collections.machineStates?.findOne({ + workflowId, + }); + + if (!restoredState) { + throw new Error("Actor not found with the provided workflowId"); + } + + console.log("restored state", restoredState); + } else { + workflowId = generateActorId(); + } + + // create the actor, a null snapshot will cause the actor to start from the initial state + const actor = createActor(machine, { + snapshot: restoredState?.persistedState, + }); + + // subscribe to the actor to persist the state to the db + actor.subscribe({ + next: async () => { + // on transition, persist the most recent actor state to the db + // be sure to enable upsert so that the state record is created if it doesn't exist! + const persistedState = actor.getPersistedSnapshot(); + console.log("persisted state", persistedState); + const result = await collections.machineStates?.replaceOne( + { + workflowId, + }, + { + workflowId, + persistedState, + }, + { upsert: true } + ); + + if (!result?.acknowledged) { + throw new Error( + "Error persisting actor state. Verify db connection is configured correctly." + ); + } + }, + error: (err) => { + console.log("Error in actor subscription: " + err); + throw err; + }, + complete: async () => { + console.log("Actor is finished!"); + actor.stop(); + }, + }); + actor.start(); + + return { actor, workflowId }; +} + +function generateActorId() { + return Math.random().toString(36).substring(2, 8); +} diff --git a/examples/mongodb-credit-check-api/services/machineLogicService.ts b/examples/mongodb-credit-check-api/services/machineLogicService.ts new file mode 100644 index 0000000000..4b8ddf3b49 --- /dev/null +++ b/examples/mongodb-credit-check-api/services/machineLogicService.ts @@ -0,0 +1,133 @@ +import { z } from "zod"; +import { collections } from "./actorService"; +import CreditReport from "../models/creditReport"; +import CreditProfile from "../models/creditProfile"; + +const userCredentialSchema = z.object({ + firstName: z.string().min(1).max(255), + lastName: z.string().min(1).max(255), + SSN: z.string().min(9).max(9), +}); + +export type userCredential = z.infer; + +// this would be a great place to lookup the user in a database and confirm they exist +// for now, we will just validate the input and return it +export async function verifyCredentials(credentials: userCredential) { + console.log("Verifying Credentials..."); + try { + userCredentialSchema.parse(credentials); + return credentials; + } catch (err) { + const errorMessage = "Invalid Credentials. Details: " + err; + console.log(errorMessage); + throw new Error(errorMessage); + } +} +// given an array of 3 scores, return the middle score +//remember, it's not production code, it's a sample! +export async function determineMiddleScore(scores: number[]) { + scores.sort(); + return scores[1]; +} + +// this is where we would check the database to see if we have an existing report for this user +// note: in real-world scenarios, you'd want to check the date of the report to see if it's stale +// for this sample, we will just return the report if it exists +export async function checkReportsTable({ + ssn, + bureauName, +}: { + ssn: string; + bureauName: string; +}) { + console.log("Checking for an existing report...."); + try { + const report = await collections.creditReports?.findOne({ + ssn, + bureauName, + }); + return report as CreditReport | undefined; + } catch (err) { + console.log("Error checking reports table", err); + throw err; + } +} + +// simulates a potentially long-running call to a bureau service +// returns a random number representing a credit score between 300 and 850 +export async function checkBureauService({ + ssn, + bureauName, +}: { + ssn: string; + bureauName: string; +}) { + switch (bureauName) { + case "GavUnion": + await sleep(range({ min: 1000, max: 10000 })); + return range({ min: 300, max: 850 }); + case "EquiGavin": + await sleep(range({ min: 1000, max: 10000 })); + return range({ min: 300, max: 850 }); + case "Gavperian": + await sleep(range({ min: 1000, max: 10000 })); + return range({ min: 300, max: 850 }); + } +} + +// this can indeed be a very long-running service, +// typically one that won't be local to the application +// for this sample, we will just simulate a long-running call +export async function generateInterestRate(creditScore: number) { + await sleep(range({ min: 1000, max: 10000 })); + if (creditScore > 700) { + return 3.5; + } else if (creditScore > 600) { + return 5; + } else { + return 200; + } +} + +// saves the specific credit report to the database, by SSN and bureau name +export async function saveCreditReport(report: CreditReport) { + try { + await collections.creditReports?.replaceOne( + { + ssn: report.ssn, + bureauName: report.bureauName, + }, + report, + { upsert: true } + ); + } catch (err) { + console.log("Error saving credit report", err); + throw err; + } +} +// saves the entire credit credit profile to the database +export async function saveCreditProfile(profile: CreditProfile) { + try { + await collections.creditProfiles?.replaceOne( + { + ssn: profile.SSN, + }, + profile, + { upsert: true } + ); + } catch (err) { + console.log("Error saving credit profile", err); + throw err; + } +} + +function sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +function range({ min, max }: { min: number; max: number }) { + return Math.floor(Math.random() * (max - min) + min); +} diff --git a/examples/mongodb-credit-check-api/tsconfig.json b/examples/mongodb-credit-check-api/tsconfig.json new file mode 100644 index 0000000000..6f63fa478a --- /dev/null +++ b/examples/mongodb-credit-check-api/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + } +}