From 5bfe3e1b5687c8f52b8d5f1ef8ae82b4ed1207f4 Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Thu, 13 Mar 2025 22:33:08 +0100 Subject: [PATCH 01/17] added first draft of cos-to-sql sample --- cos-to-sql/.ceignore | 1 + cos-to-sql/Dockerfile | 21 + cos-to-sql/README.md | 202 ++++++++++ cos-to-sql/package-lock.json | 762 +++++++++++++++++++++++++++++++++++ cos-to-sql/package.json | 26 ++ cos-to-sql/samples/users.csv | 2 + cos-to-sql/src/job.mjs | 231 +++++++++++ 7 files changed, 1245 insertions(+) create mode 100644 cos-to-sql/.ceignore create mode 100644 cos-to-sql/Dockerfile create mode 100644 cos-to-sql/README.md create mode 100644 cos-to-sql/package-lock.json create mode 100644 cos-to-sql/package.json create mode 100644 cos-to-sql/samples/users.csv create mode 100644 cos-to-sql/src/job.mjs diff --git a/cos-to-sql/.ceignore b/cos-to-sql/.ceignore new file mode 100644 index 00000000..b512c09d --- /dev/null +++ b/cos-to-sql/.ceignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/cos-to-sql/Dockerfile b/cos-to-sql/Dockerfile new file mode 100644 index 00000000..ea477c59 --- /dev/null +++ b/cos-to-sql/Dockerfile @@ -0,0 +1,21 @@ +FROM registry.access.redhat.com/ubi9/nodejs-22:latest AS build-env +WORKDIR /app + +# Copy job dependency manifests to the container image. +COPY package.json ./ + +# Install production dependencies. +RUN npm install --production + +# Use a small distroless image for as runtime image +FROM gcr.io/distroless/nodejs22-debian12 + +WORKDIR /app + +# Copy local code to the container image. +COPY ./src ./ + +# Copy the dependencies and other project related files +COPY --from=build-env /app ./ + +CMD ["job.mjs"] \ No newline at end of file diff --git a/cos-to-sql/README.md b/cos-to-sql/README.md new file mode 100644 index 00000000..f68e30d7 --- /dev/null +++ b/cos-to-sql/README.md @@ -0,0 +1,202 @@ +# IBM Cloud Code Engine - Integrate Cloud Object Storage and PostgreSQL through a job and an event subscription + +This sample demonstrates how to read CSV files hosted on a IBM Cloud Object Storage and save their contents line by line into relational PostgreSQL database. + +## Prerequisites + +Make sure the following [IBM Cloud CLI](https://cloud.ibm.com/docs/cli/reference/ibmcloud?topic=cloud-cli-getting-started) and the following list of plugins are installed +- `ibmcloud plugin install code-engine` +- `ibmcloud plugin install cloud-object-storage` + +Install `jq`. On MacOS, you can use following [brew formulae](https://formulae.brew.sh/formula/jq) to do a `brew install jq`. +## CLI Setup + +Login to IBM Cloud via the CLI +``` +ibmcloud login +``` + +Target the `ca-tor` region: +``` +export REGION=ca-tor +ibmcloud -r $REGION +``` + +Create the project: +``` +ibmcloud code-engine project create -n ce-objectstorage-to-sql +``` + +Store the project guid: +``` +export CE_ID=$(ibmcloud ce project current -o json | jq -r .guid) +``` + +Create the job: +``` +ibmcloud code-engine job create \ + --name csv-to-sql \ + --source ./ \ + --retrylimit 0 \ + --cpu 0.25 \ + --memory 0.5G \ + --wait +``` + +Create the COS instance: +``` +ibmcloud resource service-instance-create csv-to-sql-cos cloud-object-storage standard global +``` + +Store the COS CRN: +``` +export COS_ID=$(ibmcloud resource service-instance csv-to-sql-cos --output json | jq -r '.[0] | .id') +``` + +Create an authorization policy to allow the Code Engine project receive events from COS: +``` +ibmcloud iam authorization-policy-create codeengine cloud-object-storage \ + "Notifications Manager" \ + --source-service-instance-id $CE_ID \ + --target-service-instance-id $COS_ID +``` + +Create a COS bucket: +``` +ibmcloud cos config crn --crn $COS_ID --force +ibmcloud cos config auth --method IAM +ibmcloud cos config region --region $REGION +ibmcloud cos config endpoint-url --url s3.$REGION.cloud-object-storage.appdomain.cloud +export BUCKET=$CE_ID-csv-to-sql +ibmcloud cos bucket-create \ + --class smart \ + --bucket $BUCKET +``` + +Creating a trusted profile that grants a Code Engine Job access to your COS bucket +``` +REGION=eu-es +RESOURCE_GROUP=Default +COS_INSTANCE_NAME=csv-to-sql-cos +COS_BUCKET=$CE_ID-csv-to-sql +CE_PROJECT_NAME=ce-objectstorage-to-sql +JOB_NAME=csv-to-sql +TRUSTED_PROFILE_NAME=code-engine-cos-access + +CE_PROJECT_CRN=$(ibmcloud resource service-instance ${CE_PROJECT_NAME} --location ${REGION} -g ${RESOURCE_GROUP} --crn 2>/dev/null | grep ':codeengine:') +COS_INSTANCE_ID=$(ibmcloud resource service-instance ${COS_INSTANCE_NAME} --crn 2>/dev/null | grep ':cloud-object-storage:') + +ibmcloud iam trusted-profile-create ${TRUSTED_PROFILE_NAME} +ibmcloud iam trusted-profile-link-create ${TRUSTED_PROFILE_NAME} --name ce-job-${JOB_NAME} --cr-type CE --link-crn ${CE_PROJECT_CRN} --link-component-type job --link-component-name ${JOB_NAME} +ibmcloud iam trusted-profile-policy-create ${TRUSTED_PROFILE_NAME} --roles "Content Reader" --service-name cloud-object-storage --service-instance ${COS_INSTANCE_ID} --resource-type bucket --resource ${COS_BUCKET} +``` + +Create the subscription for all COS events: +``` +ibmcloud ce sub cos create \ + --name coswatch \ + --bucket $BUCKET \ + --destination csv-to-sql \ + --destination-type job +``` + +Create a PostgreSQL service instance: +``` +ibmcloud resource service-instance-create csv-to-sql-postgresql databases-for-postgresql standard $REGION --service-endpoints private -p \ + '{ + "disk_encryption_instance_crn": "none", + "disk_encryption_key_crn": "none", + "members_cpu_allocation_count": "0 cores", + "members_disk_allocation_mb": "10240MB", + "members_host_flavor": "multitenant", + "members_members_allocation_count": 2, + "members_memory_allocation_mb": "8192MB", + "service-endpoints": "private", + "version": "16" +}' +``` + +Create a trusted profile that grants this Code Engine job access to the COS instance +``` +REGION=eu-es +RESOURCE_GROUP=Default +COS_INSTANCE_NAME=my-first-cos +COS_BUCKET=my-first-bucket-31292 +CE_PROJECT_NAME=trusted-profiles-test +JOB_NAME=list-cos-files +COS_TRUSTED_PROFILE_NAME=code-engine-cos-access +SM_TRUSTED_PROFILE_NAME=code-engine-sm-access +SM_SERVICE_URL=https://4e61488a-b76f-4d44-ba0e-ed2489d8a57a.private.eu-es.secrets-manager.appdomain.cloud +SM_PG_SECRET_ID= + +CE_PROJECT_CRN=$(ibmcloud resource service-instance ${CE_PROJECT_NAME} --location ${REGION} -g ${RESOURCE_GROUP} --crn 2>/dev/null | grep ':codeengine:') +COS_INSTANCE_ID=$(ibmcloud resource service-instance ${COS_INSTANCE_NAME} --crn 2>/dev/null | grep ':cloud-object-storage:') + +ibmcloud iam trusted-profile-create ${TRUSTED_PROFILE_NAME} +ibmcloud iam trusted-profile-link-create ${TRUSTED_PROFILE_NAME} --name ce-job-${JOB_NAME} --cr-type CE --link-crn ${CE_PROJECT_CRN} --link-component-type job --link-component-name ${JOB_NAME} +ibmcloud iam trusted-profile-policy-create ${TRUSTED_PROFILE_NAME} --roles "Content Reader" --service-name cloud-object-storage --service-instance ${COS_INSTANCE_ID} --resource-type bucket --resource ${COS_BUCKET} +``` + +Update the job by adding a binding to the PostgreSQL instance: +``` +ibmcloud code-engine job update \ + --name csv-to-sql \ + --trusted-profiles-enabled true \ + --env COS_REGION=${REGION} \ + --env COS_TRUSTED_PROFILE_NAME=${COS_TRUSTED_PROFILE_NAME} \ + --env SM_TRUSTED_PROFILE_NAME=${SM_TRUSTED_PROFILE_NAME}\ + --env SM_SERVICE_URL=${SM_SERVICE_URL} \ + --env SM_PG_SECRET_ID=${SM_PG_SECRET_ID} +``` + +Create a Secrets Manager instance +``` +ibmcloud resource service-instance-create credential-store secrets-manager 7713c3a8-3be8-4a9a-81bb-ee822fcaac3d eu-es -p \ +'{ + "allowed_network": "private-only" +}' +``` + +// Create a Trusted Profile for Secrets Manager + +// Create a S2S policy "Key Manager" between SM and the DB + +// Create a service credential for PG with automatic key rotation + + + +// ibmcloud secrets-manager secret-by-name --secret-type service_credentials --name pg-credentials --secret-group-name --service-url https://4e61488a-b76f-4d44-ba0e-ed2489d8a57a.private.eu-es.secrets-manager.appdomain.cloud + +// https://github.com/IBM/secrets-manager-node-sdk + +Upload a CSV file to COS, to initate an event that leads to a job execution: +``` +ibmcloud cos object-put \ + --bucket $BUCKET \ + --key users.csv \ + --body ./samples/users.csv \ + --content-type text/csv +``` + +List all jobs to determine the one, that processes the COS bucket update: +``` +ibmcloud code-engine jobrun list \ + --job csv-to-sql \ + --sort-by age +``` + +Inspect the job execution by opening the logs: +``` +ibmcloud code-engine jobrun logs \ + --name +``` + +Or do the two commands in one, using this one-liner: +``` +jobrunname=$(ibmcloud ce jr list -j csv-to-sql -s age -o json | jq -r '.items[0] | .metadata.name') && ibmcloud ce jr logs -n $jobrunname -f +``` + + +``` +kubectl patch jobdefinitions csv-to-sql --type='json' -p='[{"op": "add", "path": "/spec/template/mountComputeResourceToken", "value":true}]' +``` \ No newline at end of file diff --git a/cos-to-sql/package-lock.json b/cos-to-sql/package-lock.json new file mode 100644 index 00000000..6878eaca --- /dev/null +++ b/cos-to-sql/package-lock.json @@ -0,0 +1,762 @@ +{ + "name": "ce-cos-to-sql", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ce-cos-to-sql", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@ibm-cloud/secrets-manager": "^2.0.9", + "csv-parser": "^3.2.0", + "ibm-cloud-sdk-core": "^5.3.0", + "pg": "^8.14.0", + "pg-connection-string": "^2.7.0" + } + }, + "node_modules/@ibm-cloud/secrets-manager": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@ibm-cloud/secrets-manager/-/secrets-manager-2.0.9.tgz", + "integrity": "sha512-5q6m1eHA+ZQ+J70++i41ASgDO+cPCVfmhv/WRspWyKfH4BmC47mm7+hl3fcGc6+JjoNjuTbn4i9O7XjIIdZ/CA==", + "dependencies": { + "extend": "^3.0.2", + "ibm-cloud-sdk-core": "^5.1.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, + "node_modules/@types/node": { + "version": "18.19.80", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.80.tgz", + "integrity": "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csv-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz", + "integrity": "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==", + "bin": { + "csv-parser": "bin/csv-parser" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ibm-cloud-sdk-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ibm-cloud-sdk-core/-/ibm-cloud-sdk-core-5.3.2.tgz", + "integrity": "sha512-YhtS+7hGNO61h/4jNShHxbbuJ1TnDqiFKQzfEaqePnonOvv8NnxWxOk92FlKKCCzZNOT34Gnd7WCLVJTntwEFQ==", + "dependencies": { + "@types/debug": "^4.1.12", + "@types/node": "^18.19.80", + "@types/tough-cookie": "^4.0.0", + "axios": "^1.8.2", + "camelcase": "^6.3.0", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "extend": "3.0.2", + "file-type": "16.5.4", + "form-data": "4.0.0", + "isstream": "0.1.2", + "jsonwebtoken": "^9.0.2", + "mime-types": "2.1.35", + "retry-axios": "^2.6.0", + "tough-cookie": "^4.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/pg": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.14.0.tgz", + "integrity": "sha512-nXbVpyoaXVmdqlKEzToFf37qzyeeh7mbiXsnoWvstSqohj88yaa/I/Rq/HEVn2QPSZEuLIJa/jSpRDyzjEx4FQ==", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.8.0", + "pg-protocol": "^1.8.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.8.0.tgz", + "integrity": "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz", + "integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readable-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", + "dependencies": { + "readable-stream": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/retry-axios": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-2.6.0.tgz", + "integrity": "sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ==", + "engines": { + "node": ">=10.7.0" + }, + "peerDependencies": { + "axios": "*" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/cos-to-sql/package.json b/cos-to-sql/package.json new file mode 100644 index 00000000..ef185399 --- /dev/null +++ b/cos-to-sql/package.json @@ -0,0 +1,26 @@ +{ + "name": "ce-cos-to-sql", + "version": "1.0.0", + "description": "", + "main": "job.mjs", + "scripts": { + "start": "node .", + "local": "node ./src/job.mjs", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "code-engine", + "cos", + "cloud-object-storage", + "csv" + ], + "license": "MIT", + "homepage": "https://cloud.ibm.com/containers/serverless", + "dependencies": { + "@ibm-cloud/secrets-manager": "^2.0.9", + "csv-parser": "^3.2.0", + "ibm-cloud-sdk-core": "^5.3.0", + "pg": "^8.14.0", + "pg-connection-string": "^2.7.0" + } +} diff --git a/cos-to-sql/samples/users.csv b/cos-to-sql/samples/users.csv new file mode 100644 index 00000000..4e632736 --- /dev/null +++ b/cos-to-sql/samples/users.csv @@ -0,0 +1,2 @@ +"Firstname","Lastname" +Foo,"Bar" \ No newline at end of file diff --git a/cos-to-sql/src/job.mjs b/cos-to-sql/src/job.mjs new file mode 100644 index 00000000..e80085c6 --- /dev/null +++ b/cos-to-sql/src/job.mjs @@ -0,0 +1,231 @@ +// library needed to materialize the authentication to COS and to SecretsManager +// and to read service credentials from SecretsManager +import { ContainerAuthenticator } from "ibm-cloud-sdk-core"; +import SecretsManager from "@ibm-cloud/secrets-manager/secrets-manager/v2.js"; + +// library to convert CSV content into a object structure +import csv from "csv-parser"; +import { Readable } from "stream"; + +// library to access PostgreSQL +import pg from "pg"; +import pgConnectionString from "pg-connection-string"; + +console.info("Starting CSV to SQL conversion ..."); + +const run = async () => { + // + // assess whether the jobrun execution contains information about the COS file that got updated + if (!process.env.CE_DATA) { + console.log("< ABORT - job does not contain any event data"); + return process.exit(1); + } + const eventData = JSON.parse(process.env.CE_DATA); + console.log(`eventData: '${JSON.stringify(eventData)}'`); + + // + // make sure that the event relates to a COS write operation + if (eventData.operation !== "Object:Write") { + console.log(`< ABORT - COS operation '${eventData.operation}' does not match expectations 'Object:Write'`); + return process.exit(1); + } + if (eventData.notification.content_type !== "text/csv") { + console.log( + `< ABORT - COS update did happen on file '${eventData.key}' which is of type '${eventData.notification.content_type}' (expected type 'text/csv')` + ); + return process.exit(1); + } + console.log(`Received a COS update event on the CSV file '${eventData.key}' in bucket '${eventData.bucket}'`); + + const cosRegion = process.env.COS_REGION; + if (!cosRegion) { + console.error("environment variable COS_REGION is not set"); + process.exit(1); + } + + const cosTrustedProfileName = process.env.COS_TRUSTED_PROFILE_NAME; + if (!cosTrustedProfileName) { + console.error("environment variable COS_TRUSTED_PROFILE_NAME is not set"); + process.exit(1); + } + + // create an authenticator to access the COS instance based on a trusted profile + const cosAuthenticator = new ContainerAuthenticator({ + iamProfileName: cosTrustedProfileName, + }); + + // + // retrieve the COS object that got updated + console.log(`Retrieving file content of '${eventData.key}' from bucket ${eventData.bucket} ...`); + const fileContent = await getObjectContent(cosAuthenticator, cosRegion, eventData.bucket, eventData.key); + + // + // convert CSV to a object structure + console.log(`Converting CSV data to a data struct ...`); + const users = await convertCsvToDataStruct(fileContent); + console.log(`users: ${JSON.stringify(users)}`); + + const smTrustedProfileName = process.env.SM_TRUSTED_PROFILE_NAME; + if (!smTrustedProfileName) { + console.error("environment variable SM_TRUSTED_PROFILE_NAME is not set"); + process.exit(1); + } + + const smServiceURL = process.env.SM_SERVICE_URL; + if (!smServiceURL) { + console.error("environment variable SM_SERVICE_URL is not set"); + process.exit(1); + } + + // create an authenticator to access the SecretsManager instance based on a trusted profile + const smAuthenticator = new ContainerAuthenticator({ + iamProfileName: smTrustedProfileName, + }); + + // Create an instance of the SDK by providing an authentication mechanism and your Secrets Manager instance URL + const secretsManager = new SecretsManager({ + authenticator: smAuthenticator, + serviceUrl: smServiceURL, + }); + + const smPgSecretId = process.env.SM_PG_SECRET_ID; + if (!smPgSecretId) { + console.error("environment variable SM_PG_SECRET_ID is not set"); + process.exit(1); + } + + // Use the Secrets Manager API to get the secret using the secret ID + const res = await secretsManager.getSecret({ + id: smPgSecretId, + }); + + console.log(`Secret '${smPgSecretId}' content: '${JSON.stringify(res.result)}'`); + + // + // Connect to PostgreSQL + // https://node-postgres.com/ + console.log(`Establishing connection to PostgreSQL database using SM secret '${res.result.name}' (last updated: '${res.result.updated_at}') ...`); + const pgCaCert = Buffer.from(res.result.credentials.connection.postgres.certificate.certificate_base64, "base64"); + const pgConnectionString = res.result.credentials.connection.postgres.composed[0]; + const pgClient = await connectDb(pgConnectionString, pgCaCert); + + // Do something meaningful with the data + // https://github.com/IBM-Cloud/compose-postgresql-helloworld-nodejs/blob/master/server.js + console.log(`Writing converted CSV data to the PostgreSQL database ...`); + const insertOperations = []; + users.forEach((userToAdd) => { + insertOperations.push(addUser(pgClient, userToAdd.Firstname, userToAdd.Lastname)); + }); + + // Wait for all SQL insert operations to finish + console.log(`Waiting for all SQL INSERT operations to finish ...`); + Promise.all(insertOperations) + .then((results) => { + results.forEach((result, idx) => console.log(`Added ${JSON.stringify(users[idx])} -> ${JSON.stringify(result)}`)); + console.info("COMPLETED"); + }) + .catch((err) => { + console.error("Failed to add users to the database", err); + console.info("FAILED"); + }); +}; +run(); + +async function getObjectContent(authenticator, region, bucket, key) { + // prepare the request to download the content of a file + const requestOptions = { + method: "GET", + }; + + // authenticate the request + await authenticator.authenticate(requestOptions); + + // perform the request + const response = await fetch( + `https://s3.direct.${region}.cloud-object-storage.appdomain.cloud/${bucket}/${key}`, + requestOptions + ); + + if (response.status !== 200) { + console.error(`Unexpected status code: ${response.status}`); + process.exit(1); + } + + // read the response + const responseBody = await response.text(); + + return responseBody; +} + +function convertCsvToDataStruct(csvContent) { + return new Promise((resolve) => { + // the result to return + const results = []; + + // create a new readable stream + var readableStream = new Readable(); + + // the CSV parser consumes the stream + readableStream + .pipe(csv()) + .on("data", (data) => results.push(data)) + .on("end", () => { + console.log(`converted CSV data: ${JSON.stringify(results)}`); + + resolve(results); + }); + + // push the CSV file content to the stream + readableStream.push(csvContent); + readableStream.push(null); // indicates end-of-file + }); +} + +function connectDb(connectionString, caCert) { + return new Promise((resolve, reject) => { + const postgreConfig = pgConnectionString.parse(connectionString); + + // Add some ssl + postgreConfig.ssl = { + ca: caCert, + }; + + // set up a new client using our config details + let client = new pg.Client(postgreConfig); + + client.connect((err) => { + if (err) { + console.error(`Failed to connect to postgreSQL host '${postgreConfig.host}'`, err); + return reject(err); + } + + client.query( + "CREATE TABLE IF NOT EXISTS users (firstname varchar(256) NOT NULL, lastname varchar(256) NOT NULL)", + (err, result) => { + if (err) { + console.log(`Failed to create PostgreSQL table 'users'`, err); + return reject(err); + } + console.log( + `Established PostgreSQL client connection to '${postgreConfig.host}' - user table init: ${JSON.stringify( + result + )}` + ); + return resolve(client); + } + ); + }); + }); +} + +function addUser(client, firstName, lastName) { + return new Promise(function (resolve, reject) { + const queryText = "INSERT INTO users(firstname,lastname) VALUES($1, $2)"; + client.query(queryText, [firstName, lastName], function (error, result) { + if (error) { + return reject(error); + } + return resolve(result); + }); + }); +} From 9e71640d9c0ac5083a3091f8c22f3af339c710c8 Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Fri, 14 Mar 2025 00:28:41 +0100 Subject: [PATCH 02/17] replaced job with an app --- cos-to-sql/Dockerfile | 4 +- cos-to-sql/Dockerfile.job | 21 ++ cos-to-sql/README.md | 22 ++ cos-to-sql/samples/users.csv | 3 +- cos-to-sql/src/app.mjs | 408 +++++++++++++++++++++++++++++++++++ 5 files changed, 455 insertions(+), 3 deletions(-) create mode 100644 cos-to-sql/Dockerfile.job create mode 100644 cos-to-sql/src/app.mjs diff --git a/cos-to-sql/Dockerfile b/cos-to-sql/Dockerfile index ea477c59..f1d7eddc 100644 --- a/cos-to-sql/Dockerfile +++ b/cos-to-sql/Dockerfile @@ -17,5 +17,5 @@ COPY ./src ./ # Copy the dependencies and other project related files COPY --from=build-env /app ./ - -CMD ["job.mjs"] \ No newline at end of file +EXPOSE 8080 +CMD ["app.mjs"] \ No newline at end of file diff --git a/cos-to-sql/Dockerfile.job b/cos-to-sql/Dockerfile.job new file mode 100644 index 00000000..ea477c59 --- /dev/null +++ b/cos-to-sql/Dockerfile.job @@ -0,0 +1,21 @@ +FROM registry.access.redhat.com/ubi9/nodejs-22:latest AS build-env +WORKDIR /app + +# Copy job dependency manifests to the container image. +COPY package.json ./ + +# Install production dependencies. +RUN npm install --production + +# Use a small distroless image for as runtime image +FROM gcr.io/distroless/nodejs22-debian12 + +WORKDIR /app + +# Copy local code to the container image. +COPY ./src ./ + +# Copy the dependencies and other project related files +COPY --from=build-env /app ./ + +CMD ["job.mjs"] \ No newline at end of file diff --git a/cos-to-sql/README.md b/cos-to-sql/README.md index f68e30d7..58996028 100644 --- a/cos-to-sql/README.md +++ b/cos-to-sql/README.md @@ -43,6 +43,20 @@ ibmcloud code-engine job create \ --wait ``` +Create the app: +``` +ibmcloud code-engine app create \ + --name csv-to-sql-app \ + --source ./ \ + --cpu 0.25 \ + --memory 0.5G \ + --env COS_REGION=eu-es \ + --env COS_TRUSTED_PROFILE_NAME=code-engine-cos-access \ + --env SM_TRUSTED_PROFILE_NAME=code-engine-sm-access \ + --env SM_SERVICE_URL=https://4e61488a-b76f-4d44-ba0e-ed2489d8a57a.private.eu-es.secrets-manager.appdomain.cloud \ + --env SM_PG_SECRET_ID=04abb32c-bbe3-a069-e4c4-8899853ef053 +``` + Create the COS instance: ``` ibmcloud resource service-instance-create csv-to-sql-cos cloud-object-storage standard global @@ -98,6 +112,13 @@ ibmcloud ce sub cos create \ --bucket $BUCKET \ --destination csv-to-sql \ --destination-type job + +ibmcloud ce sub cos create \ + --name coswatch \ + --bucket $BUCKET \ + --destination csv-to-sql-app \ + --destination-type app \ + --path /cos-to-sql ``` Create a PostgreSQL service instance: @@ -199,4 +220,5 @@ jobrunname=$(ibmcloud ce jr list -j csv-to-sql -s age -o json | jq -r '.items[0] ``` kubectl patch jobdefinitions csv-to-sql --type='json' -p='[{"op": "add", "path": "/spec/template/mountComputeResourceToken", "value":true}]' +kubectl patch ksvc csv-to-sql-app --type='json' -p='[{"op": "add", "path": "/spec/template/metadata/annotations/codeengine.cloud.ibm.com~1mount-compute-resource-token", "value":"true"}]' ``` \ No newline at end of file diff --git a/cos-to-sql/samples/users.csv b/cos-to-sql/samples/users.csv index 4e632736..c8ca83f9 100644 --- a/cos-to-sql/samples/users.csv +++ b/cos-to-sql/samples/users.csv @@ -1,2 +1,3 @@ "Firstname","Lastname" -Foo,"Bar" \ No newline at end of file +Foo,"Bar" +Some,"thing" \ No newline at end of file diff --git a/cos-to-sql/src/app.mjs b/cos-to-sql/src/app.mjs new file mode 100644 index 00000000..eebf6412 --- /dev/null +++ b/cos-to-sql/src/app.mjs @@ -0,0 +1,408 @@ +import { existsSync } from "fs"; +import http from "http"; + +// Libraries needed to materialize the authentication to COS and to SecretsManager +// and to read service credentials from SecretsManager +import SecretsManager from "@ibm-cloud/secrets-manager/secrets-manager/v2.js"; +import { ContainerAuthenticator } from "ibm-cloud-sdk-core"; + +// Libraries to convert CSV content into a object structure +import csv from "csv-parser"; +import { Readable } from "stream"; + +// Libraries to access PostgreSQL +import pg from "pg"; +import pgConnectionString from "pg-connection-string"; + +console.info("Starting the app ..."); + +// +// Initialize COS +const cosRegion = process.env.COS_REGION; +const cosTrustedProfileName = process.env.COS_TRUSTED_PROFILE_NAME; +let cosAuthenticator; +if (cosTrustedProfileName) { + // create an authenticator to access the COS instance based on a trusted profile + cosAuthenticator = new ContainerAuthenticator({ + iamProfileName: cosTrustedProfileName, + }); +} + +// +// Initialize Secrets Manager SDK +const smTrustedProfileName = process.env.SM_TRUSTED_PROFILE_NAME; +const smServiceURL = process.env.SM_SERVICE_URL; +const smPgSecretId = process.env.SM_PG_SECRET_ID; +let secretsManager; +if (smTrustedProfileName && smServiceURL) { + // create an authenticator to access the SecretsManager instance based on a trusted profile + const smAuthenticator = new ContainerAuthenticator({ + iamProfileName: smTrustedProfileName, + }); + // Create an instance of the SDK by providing an authentication mechanism and your Secrets Manager instance URL + secretsManager = new SecretsManager({ + authenticator: smAuthenticator, + serviceUrl: smServiceURL, + }); +} + +let _pgClient; + +const server = http + .createServer(async function (request, response) { + // + // Readiness endpoint + if (request.url == "/readiness") { + if (!cosRegion) { + console.error("environment variable COS_REGION is not set"); + response.writeHead(500, { "Content-Type": "application/json" }); + response.end('{"error": "environment variable COS_REGION is not set"}'); + return; + } + + if (!cosTrustedProfileName) { + console.error("environment variable COS_TRUSTED_PROFILE_NAME is not set"); + response.writeHead(500, { "Content-Type": "application/json" }); + response.end('{"error": "environment variable COS_TRUSTED_PROFILE_NAME is not set"}'); + return; + } + + if (!smTrustedProfileName) { + console.error("environment variable SM_TRUSTED_PROFILE_NAME is not set"); + response.writeHead(500, { "Content-Type": "application/json" }); + response.end('{"error": "environment variable SM_TRUSTED_PROFILE_NAME is not set"}'); + return; + } + + if (!smServiceURL) { + console.error("environment variable SM_SERVICE_URL is not set"); + response.writeHead(500, { "Content-Type": "application/json" }); + response.end('{"error": "environment variable SM_SERVICE_URL is not set"}'); + return; + } + + if (!smPgSecretId) { + console.error("environment variable SM_PG_SECRET_ID is not set"); + response.writeHead(500, { "Content-Type": "application/json" }); + response.end('{"error": "environment variable SM_PG_SECRET_ID is not set"}'); + return; + } + + if (!existsSync("/var/run/secrets/codeengine.cloud.ibm.com/compute-resource-token/token")) { + console.error("Mounting the trusted profile compute resource token is not enabled"); + response.writeHead(500, { "Content-Type": "application/json" }); + response.end('{"error": "Mounting the trusted profile compute resource token is not enabled"}'); + return; + } + + response.writeHead(200, { "Content-Type": "application/json" }); + response.end('{"status": "ok"}'); + return; + } + + // + // Ingestion endpoint + if (request.url == "/cos-to-sql") { + const body = await getBody(request); + console.log(`request body: '${body}'`); + console.log(`request headers: '${JSON.stringify(request.headers)}`); + + // + // assess whether the jobrun execution contains information about the COS file that got updated + if (!body) { + console.log("Request does not contain any event data"); + response.writeHead(400, { "Content-Type": "application/json" }); + response.end('{"error": "request does not contain any event data"}'); + return; + } + const eventData = JSON.parse(body); + console.log(`eventData: '${JSON.stringify(eventData)}'`); + + // + // make sure that the event relates to a COS write operation + if (eventData.notification.event_type !== "Object:Write") { + console.log(`COS operation '${eventData.notification.event_type}' does not match expectations 'Object:Write'`); + response.writeHead(400, { "Content-Type": "application/json" }); + response.end( + `{"error": "COS operation '${eventData.notification.event_type}' does not match expectations 'Object:Write'"}` + ); + return; + } + if (eventData.notification.content_type !== "text/csv") { + console.log( + `COS update did happen on file '${eventData.key}' which is of type '${eventData.notification.content_type}' (expected type 'text/csv')` + ); + response.writeHead(400, { "Content-Type": "application/json" }); + response.end( + `{"error": "COS update did happen on file '${eventData.key}' which is of type '${eventData.notification.content_type}' (expected type 'text/csv')"}` + ); + return; + } + console.log(`Received a COS update event on the CSV file '${eventData.key}' in bucket '${eventData.bucket}'`); + + // + // retrieve the COS object that got updated + console.log(`Retrieving file content of '${eventData.key}' from bucket ${eventData.bucket} ...`); + const fileContent = await getObjectContent(cosAuthenticator, cosRegion, eventData.bucket, eventData.key); + + // + // convert CSV to a object structure + console.log(`Converting CSV data to a data struct ...`); + const users = await convertCsvToDataStruct(fileContent); + console.log(`users: ${JSON.stringify(users)}`); + + const pgClient = await getPgClient(secretsManager, smPgSecretId); + + // Do something meaningful with the data + // https://github.com/IBM-Cloud/compose-postgresql-helloworld-nodejs/blob/master/server.js + console.log(`Writing converted CSV data to the PostgreSQL database ...`); + const insertOperations = []; + users.forEach((userToAdd) => { + insertOperations.push(addUser(pgClient, userToAdd.Firstname, userToAdd.Lastname)); + }); + + // Wait for all SQL insert operations to finish + console.log(`Waiting for all SQL INSERT operations to finish ...`); + await Promise.all(insertOperations) + .then((results) => { + results.forEach((result, idx) => + console.log(`Added ${JSON.stringify(users[idx])} -> ${JSON.stringify(result)}`) + ); + console.info("COMPLETED"); + return Promise.resolve(); + }) + .catch((err) => { + console.error("Failed to add users to the database", err); + console.info("FAILED"); + return Promise.reject(); + }); + + console.log(`Insertions done!`); + response.writeHead(200, { "Content-Type": "application/json" }); + response.end(`{"status": "done"}`); + return; + } + + // + // Endpoint that drops the users table + if (request.url == "/clear") { + const pgClient = await getPgClient(secretsManager, smPgSecretId); + await dropUsers(pgClient); + console.log(`Deletions done!`); + response.writeHead(200, { "Content-Type": "application/json" }); + response.end(`{"status": "done"}`); + return; + } + + // + // Default http endpoint, which prints a simple hello world + const pgClient = await getPgClient(secretsManager, smPgSecretId); + const allUsers = await listUsers(pgClient); + response.writeHead(200, { "Content-Type": "application/json" }); + response.end(JSON.stringify({ users: allUsers.rows })); + }) + .listen(8080); + +console.log("Server running at http://0.0.0.0:8080/"); + +process.on("SIGTERM", () => { + console.info("SIGTERM signal received."); + server.close(() => { + console.log("Http server closed."); + + if (_pgClient) { + _pgClient.end(); + console.log("PG client ended."); + } + }); +}); + +async function getObjectContent(authenticator, region, bucket, key) { + const fn = "getObjectContent "; + const startTime = Date.now(); + console.log(`${fn} > region: '${region}', bucket: '${bucket}', key: '${key}'`); + + // prepare the request to download the content of a file + const requestOptions = { + method: "GET", + }; + + // authenticate the request + await authenticator.authenticate(requestOptions); + + // perform the request + const response = await fetch( + `https://s3.direct.${region}.cloud-object-storage.appdomain.cloud/${bucket}/${key}`, + requestOptions + ); + + if (response.status !== 200) { + const err = new Error(`Unexpected status code: ${response.status}`); + console.log(`${fn} < failed - error: ${err.message}; duration ${Date.now() - startTime} ms`); + return Promise.reject(err); + } + + // read the response + const responseBody = await response.text(); + + console.log(`${fn} < done - duration ${Date.now() - startTime} ms`); + return responseBody; +} + +function convertCsvToDataStruct(csvContent) { + return new Promise((resolve) => { + // the result to return + const results = []; + + // create a new readable stream + var readableStream = new Readable(); + + // the CSV parser consumes the stream + readableStream + .pipe(csv()) + .on("data", (data) => results.push(data)) + .on("end", () => { + console.log(`converted CSV data: ${JSON.stringify(results)}`); + + resolve(results); + }); + + // push the CSV file content to the stream + readableStream.push(csvContent); + readableStream.push(null); // indicates end-of-file + }); +} + +function connectDb(connectionString, caCert) { + return new Promise((resolve, reject) => { + const postgreConfig = pgConnectionString.parse(connectionString); + + // Add some ssl + postgreConfig.ssl = { + ca: caCert, + }; + + // set up a new client using our config details + let client = new pg.Client(postgreConfig); + + client.connect((err) => { + if (err) { + console.error(`Failed to connect to postgreSQL host '${postgreConfig.host}'`, err); + return reject(err); + } + + client.query( + "CREATE TABLE IF NOT EXISTS users (firstname varchar(256) NOT NULL, lastname varchar(256) NOT NULL)", + (err, result) => { + if (err) { + console.log(`Failed to create PostgreSQL table 'users'`, err); + return reject(err); + } + console.log( + `Established PostgreSQL client connection to '${postgreConfig.host}' - user table init: ${JSON.stringify( + result + )}` + ); + return resolve(client); + } + ); + }); + }); +} + +async function getPgClient(secretsManager, secretId) { + const fn = "getPgClient "; + const startTime = Date.now(); + console.log(`${fn} >`); + + // Check whether the pg client had been initialized already + if (_pgClient) { + console.log(`${fn} < from local cache`); + return Promise.resolve(_pgClient); + } + + console.log(`Fetching secret '${secretId}' ...`); + // Use the Secrets Manager API to get the secret using the secret ID + const res = await secretsManager.getSecret({ + id: secretId, + }); + console.log(`Secret '${secretId}' fetched in ${Date.now() - startTime} ms`); + + // + // Connect to PostgreSQL + // https://node-postgres.com/ + console.log( + `Establishing connection to PostgreSQL database using SM secret '${res.result.name}' (last updated: '${res.result.updated_at}') ...` + ); + const pgCaCert = Buffer.from(res.result.credentials.connection.postgres.certificate.certificate_base64, "base64"); + const pgConnectionString = res.result.credentials.connection.postgres.composed[0]; + _pgClient = await connectDb(pgConnectionString, pgCaCert); + + console.log(`${fn} < done - duration ${Date.now() - startTime} ms`); + return _pgClient; +} + +function addUser(client, firstName, lastName) { + const fn = "addUser "; + const startTime = Date.now(); + console.log(`${fn} > firstName: '${firstName}', lastName: '${lastName}'`); + return new Promise(function (resolve, reject) { + const queryText = "INSERT INTO users(firstname,lastname) VALUES($1, $2)"; + client.query(queryText, [firstName, lastName], function (error, result) { + if (error) { + console.log(`${fn} < failed - error: ${error}; duration ${Date.now() - startTime} ms`); + return reject(error); + } + console.log(`${fn} < succeeded - duration ${Date.now() - startTime} ms`); + return resolve(result); + }); + }); +} + +function listUsers(client) { + const fn = "listUsers "; + const startTime = Date.now(); + console.log(`${fn} >`); + return new Promise(function (resolve, reject) { + const queryText = "SELECT * FROM users"; + client.query(queryText, undefined, function (error, result) { + if (error) { + console.log(`${fn} < failed - error: ${error}; duration ${Date.now() - startTime} ms`); + return reject(error); + } + console.log(`${fn} < succeeded - duration ${Date.now() - startTime} ms`); + return resolve(result); + }); + }); +} +function dropUsers(client) { + const fn = "dropUsers "; + const startTime = Date.now(); + console.log(`${fn} >`); + return new Promise(function (resolve, reject) { + const queryText = "DROP TABLE users"; + client.query(queryText, undefined, function (error, result) { + if (error) { + console.log(`${fn} < failed - error: ${error}; duration ${Date.now() - startTime} ms`); + return reject(error); + } + console.log(`${fn} < succeeded - duration ${Date.now() - startTime} ms`); + return resolve(result); + }); + }); +} + +function getBody(request) { + return new Promise((resolve) => { + const bodyParts = []; + let body; + request + .on("data", (chunk) => { + bodyParts.push(chunk); + }) + .on("end", () => { + body = Buffer.concat(bodyParts).toString(); + resolve(body); + }); + }); +} From 544f6976be280ffdf57be1332b03a3252ac3e246 Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Fri, 14 Mar 2025 00:33:18 +0100 Subject: [PATCH 03/17] adjusted /clear endpoint --- cos-to-sql/src/app.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cos-to-sql/src/app.mjs b/cos-to-sql/src/app.mjs index eebf6412..2684aff0 100644 --- a/cos-to-sql/src/app.mjs +++ b/cos-to-sql/src/app.mjs @@ -187,7 +187,7 @@ const server = http // Endpoint that drops the users table if (request.url == "/clear") { const pgClient = await getPgClient(secretsManager, smPgSecretId); - await dropUsers(pgClient); + await deleteUsers(pgClient); console.log(`Deletions done!`); response.writeHead(200, { "Content-Type": "application/json" }); response.end(`{"status": "done"}`); @@ -375,12 +375,12 @@ function listUsers(client) { }); }); } -function dropUsers(client) { - const fn = "dropUsers "; +function deleteUsers(client) { + const fn = "deleteUsers "; const startTime = Date.now(); console.log(`${fn} >`); return new Promise(function (resolve, reject) { - const queryText = "DROP TABLE users"; + const queryText = "DELETE FROM users"; client.query(queryText, undefined, function (error, result) { if (error) { console.log(`${fn} < failed - error: ${error}; duration ${Date.now() - startTime} ms`); From afea0966d30dde205491f103dd85cf6705037c4c Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Sat, 5 Apr 2025 00:15:01 +0200 Subject: [PATCH 04/17] finalized the cos-to-sql sample code and readme instructions --- cos-to-sql/.dockerignore | 5 + cos-to-sql/Dockerfile | 26 +- cos-to-sql/Dockerfile.job | 21 - cos-to-sql/README.md | 254 ++-- cos-to-sql/app.mjs | 203 +++ cos-to-sql/package-lock.json | 2287 ++++++++++++++++++++++----------- cos-to-sql/package.json | 50 +- cos-to-sql/public/favicon.ico | Bin 0 -> 2247 bytes cos-to-sql/src/app.mjs | 408 ------ cos-to-sql/src/job.mjs | 231 ---- cos-to-sql/utils/cos.mjs | 31 + cos-to-sql/utils/db.mjs | 136 ++ cos-to-sql/utils/utils.mjs | 28 + 13 files changed, 2074 insertions(+), 1606 deletions(-) create mode 100644 cos-to-sql/.dockerignore delete mode 100644 cos-to-sql/Dockerfile.job create mode 100644 cos-to-sql/app.mjs create mode 100644 cos-to-sql/public/favicon.ico delete mode 100644 cos-to-sql/src/app.mjs delete mode 100644 cos-to-sql/src/job.mjs create mode 100644 cos-to-sql/utils/cos.mjs create mode 100644 cos-to-sql/utils/db.mjs create mode 100644 cos-to-sql/utils/utils.mjs diff --git a/cos-to-sql/.dockerignore b/cos-to-sql/.dockerignore new file mode 100644 index 00000000..6bb19bf7 --- /dev/null +++ b/cos-to-sql/.dockerignore @@ -0,0 +1,5 @@ +.dockerignore +.gitignore +build +Dockerfile +node_modules \ No newline at end of file diff --git a/cos-to-sql/Dockerfile b/cos-to-sql/Dockerfile index f1d7eddc..c5284fbd 100644 --- a/cos-to-sql/Dockerfile +++ b/cos-to-sql/Dockerfile @@ -1,21 +1,21 @@ -FROM registry.access.redhat.com/ubi9/nodejs-22:latest AS build-env -WORKDIR /app +# Download dependencies in builder stage +FROM registry.access.redhat.com/ubi9/nodejs-22:latest AS builder -# Copy job dependency manifests to the container image. -COPY package.json ./ +COPY --chown=${CNB_USER_ID}:${CNB_GROUP_ID} package.json package-lock.json /app/ +WORKDIR /app +RUN npm ci --omit=dev -# Install production dependencies. -RUN npm install --production # Use a small distroless image for as runtime image -FROM gcr.io/distroless/nodejs22-debian12 - -WORKDIR /app +FROM gcr.io/distroless/nodejs22 -# Copy local code to the container image. -COPY ./src ./ +COPY --chown=1001:0 --from=builder /app/node_modules /app/node_modules +COPY --chown=1001:0 app.mjs /app +COPY --chown=1001:0 utils/ /app/utils +COPY --chown=1001:0 public/ /app/public -# Copy the dependencies and other project related files -COPY --from=build-env /app ./ +USER 1001:0 +WORKDIR /app EXPOSE 8080 + CMD ["app.mjs"] \ No newline at end of file diff --git a/cos-to-sql/Dockerfile.job b/cos-to-sql/Dockerfile.job deleted file mode 100644 index ea477c59..00000000 --- a/cos-to-sql/Dockerfile.job +++ /dev/null @@ -1,21 +0,0 @@ -FROM registry.access.redhat.com/ubi9/nodejs-22:latest AS build-env -WORKDIR /app - -# Copy job dependency manifests to the container image. -COPY package.json ./ - -# Install production dependencies. -RUN npm install --production - -# Use a small distroless image for as runtime image -FROM gcr.io/distroless/nodejs22-debian12 - -WORKDIR /app - -# Copy local code to the container image. -COPY ./src ./ - -# Copy the dependencies and other project related files -COPY --from=build-env /app ./ - -CMD ["job.mjs"] \ No newline at end of file diff --git a/cos-to-sql/README.md b/cos-to-sql/README.md index 58996028..5a74f5ea 100644 --- a/cos-to-sql/README.md +++ b/cos-to-sql/README.md @@ -1,4 +1,4 @@ -# IBM Cloud Code Engine - Integrate Cloud Object Storage and PostgreSQL through a job and an event subscription +# IBM Cloud Code Engine - Integrate Cloud Object Storage and PostgreSQL through a app and an event subscription This sample demonstrates how to read CSV files hosted on a IBM Cloud Object Storage and save their contents line by line into relational PostgreSQL database. @@ -7,123 +7,52 @@ This sample demonstrates how to read CSV files hosted on a IBM Cloud Object Stor Make sure the following [IBM Cloud CLI](https://cloud.ibm.com/docs/cli/reference/ibmcloud?topic=cloud-cli-getting-started) and the following list of plugins are installed - `ibmcloud plugin install code-engine` - `ibmcloud plugin install cloud-object-storage` +- `ibmcloud plugin install secrets-manager` Install `jq`. On MacOS, you can use following [brew formulae](https://formulae.brew.sh/formula/jq) to do a `brew install jq`. -## CLI Setup -Login to IBM Cloud via the CLI -``` -ibmcloud login -``` +## Setting up all IBM Cloud Service instances -Target the `ca-tor` region: +* Login to IBM Cloud via the CLI and target the `ca-tor` region: ``` export REGION=ca-tor -ibmcloud -r $REGION -``` - -Create the project: -``` -ibmcloud code-engine project create -n ce-objectstorage-to-sql -``` - -Store the project guid: -``` -export CE_ID=$(ibmcloud ce project current -o json | jq -r .guid) -``` - -Create the job: -``` -ibmcloud code-engine job create \ - --name csv-to-sql \ - --source ./ \ - --retrylimit 0 \ - --cpu 0.25 \ - --memory 0.5G \ - --wait +export RESOURCE_GROUP=Default +ibmcloud login -r ${REGION} -g $RESOURCE_GROUP ``` -Create the app: -``` -ibmcloud code-engine app create \ - --name csv-to-sql-app \ - --source ./ \ - --cpu 0.25 \ - --memory 0.5G \ - --env COS_REGION=eu-es \ - --env COS_TRUSTED_PROFILE_NAME=code-engine-cos-access \ - --env SM_TRUSTED_PROFILE_NAME=code-engine-sm-access \ - --env SM_SERVICE_URL=https://4e61488a-b76f-4d44-ba0e-ed2489d8a57a.private.eu-es.secrets-manager.appdomain.cloud \ - --env SM_PG_SECRET_ID=04abb32c-bbe3-a069-e4c4-8899853ef053 +* Create the Code Engine project ``` +export CE_INSTANCE_NAME=cos-to-sql--ce +ibmcloud code-engine project create -n ${CE_INSTANCE_NAME} -Create the COS instance: -``` -ibmcloud resource service-instance-create csv-to-sql-cos cloud-object-storage standard global +export CE_INSTANCE_GUID=$(ibmcloud ce project current -o json | jq -r .guid) +export CE_INSTANCE_ID=$(ibmcloud resource service-instance ${CE_INSTANCE_NAME} --output json | jq -r '.[0] | .id') ``` -Store the COS CRN: -``` -export COS_ID=$(ibmcloud resource service-instance csv-to-sql-cos --output json | jq -r '.[0] | .id') +* Create the COS instance ``` +export COS_INSTANCE_NAME=cos-to-sql--cos +ibmcloud resource service-instance-create ${COS_INSTANCE_NAME} cloud-object-storage standard global -Create an authorization policy to allow the Code Engine project receive events from COS: -``` -ibmcloud iam authorization-policy-create codeengine cloud-object-storage \ - "Notifications Manager" \ - --source-service-instance-id $CE_ID \ - --target-service-instance-id $COS_ID +export COS_INSTANCE_ID=$(ibmcloud resource service-instance ${COS_INSTANCE_NAME} --output json | jq -r '.[0] | .id') ``` -Create a COS bucket: +* Create a COS bucket ``` -ibmcloud cos config crn --crn $COS_ID --force +ibmcloud cos config crn --crn ${COS_INSTANCE_ID} --force ibmcloud cos config auth --method IAM -ibmcloud cos config region --region $REGION -ibmcloud cos config endpoint-url --url s3.$REGION.cloud-object-storage.appdomain.cloud -export BUCKET=$CE_ID-csv-to-sql +ibmcloud cos config region --region ${REGION} +ibmcloud cos config endpoint-url --url s3.${REGION}.cloud-object-storage.appdomain.cloud +export COS_BUCKET_NAME=${CE_INSTANCE_GUID}-csv-to-sql ibmcloud cos bucket-create \ --class smart \ - --bucket $BUCKET -``` - -Creating a trusted profile that grants a Code Engine Job access to your COS bucket -``` -REGION=eu-es -RESOURCE_GROUP=Default -COS_INSTANCE_NAME=csv-to-sql-cos -COS_BUCKET=$CE_ID-csv-to-sql -CE_PROJECT_NAME=ce-objectstorage-to-sql -JOB_NAME=csv-to-sql -TRUSTED_PROFILE_NAME=code-engine-cos-access - -CE_PROJECT_CRN=$(ibmcloud resource service-instance ${CE_PROJECT_NAME} --location ${REGION} -g ${RESOURCE_GROUP} --crn 2>/dev/null | grep ':codeengine:') -COS_INSTANCE_ID=$(ibmcloud resource service-instance ${COS_INSTANCE_NAME} --crn 2>/dev/null | grep ':cloud-object-storage:') - -ibmcloud iam trusted-profile-create ${TRUSTED_PROFILE_NAME} -ibmcloud iam trusted-profile-link-create ${TRUSTED_PROFILE_NAME} --name ce-job-${JOB_NAME} --cr-type CE --link-crn ${CE_PROJECT_CRN} --link-component-type job --link-component-name ${JOB_NAME} -ibmcloud iam trusted-profile-policy-create ${TRUSTED_PROFILE_NAME} --roles "Content Reader" --service-name cloud-object-storage --service-instance ${COS_INSTANCE_ID} --resource-type bucket --resource ${COS_BUCKET} -``` - -Create the subscription for all COS events: -``` -ibmcloud ce sub cos create \ - --name coswatch \ - --bucket $BUCKET \ - --destination csv-to-sql \ - --destination-type job - -ibmcloud ce sub cos create \ - --name coswatch \ - --bucket $BUCKET \ - --destination csv-to-sql-app \ - --destination-type app \ - --path /cos-to-sql + --bucket $COS_BUCKET_NAME ``` -Create a PostgreSQL service instance: +* Create the PostgreSQL instance ``` -ibmcloud resource service-instance-create csv-to-sql-postgresql databases-for-postgresql standard $REGION --service-endpoints private -p \ +export DB_INSTANCE_NAME=cos-to-sql--pg +ibmcloud resource service-instance-create $DB_INSTANCE_NAME databases-for-postgresql standard ${REGION} --service-endpoints private -p \ '{ "disk_encryption_instance_crn": "none", "disk_encryption_key_crn": "none", @@ -135,90 +64,113 @@ ibmcloud resource service-instance-create csv-to-sql-postgresql databases-for-po "service-endpoints": "private", "version": "16" }' -``` -Create a trusted profile that grants this Code Engine job access to the COS instance +export DB_INSTANCE_ID=$(ibmcloud resource service-instance $DB_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .id') ``` -REGION=eu-es -RESOURCE_GROUP=Default -COS_INSTANCE_NAME=my-first-cos -COS_BUCKET=my-first-bucket-31292 -CE_PROJECT_NAME=trusted-profiles-test -JOB_NAME=list-cos-files -COS_TRUSTED_PROFILE_NAME=code-engine-cos-access -SM_TRUSTED_PROFILE_NAME=code-engine-sm-access -SM_SERVICE_URL=https://4e61488a-b76f-4d44-ba0e-ed2489d8a57a.private.eu-es.secrets-manager.appdomain.cloud -SM_PG_SECRET_ID= -CE_PROJECT_CRN=$(ibmcloud resource service-instance ${CE_PROJECT_NAME} --location ${REGION} -g ${RESOURCE_GROUP} --crn 2>/dev/null | grep ':codeengine:') -COS_INSTANCE_ID=$(ibmcloud resource service-instance ${COS_INSTANCE_NAME} --crn 2>/dev/null | grep ':cloud-object-storage:') +* Create the Secrets Manager instance +``` +export SM_INSTANCE_NAME=cos-to-sql--sm +ibmcloud resource service-instance-create $SM_INSTANCE_NAME secrets-manager 7713c3a8-3be8-4a9a-81bb-ee822fcaac3d ${REGION} -p \ +'{ + "allowed_network": "public-and-private" +}' -ibmcloud iam trusted-profile-create ${TRUSTED_PROFILE_NAME} -ibmcloud iam trusted-profile-link-create ${TRUSTED_PROFILE_NAME} --name ce-job-${JOB_NAME} --cr-type CE --link-crn ${CE_PROJECT_CRN} --link-component-type job --link-component-name ${JOB_NAME} -ibmcloud iam trusted-profile-policy-create ${TRUSTED_PROFILE_NAME} --roles "Content Reader" --service-name cloud-object-storage --service-instance ${COS_INSTANCE_ID} --resource-type bucket --resource ${COS_BUCKET} +export SM_INSTANCE_ID=$(ibmcloud resource service-instance $SM_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .id') +export SM_INSTANCE_GUID=$(ibmcloud resource service-instance $SM_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .guid') +export SECRETS_MANAGER_URL=https://$SM_INSTANCE_GUID.${REGION}.secrets-manager.appdomain.cloud ``` -Update the job by adding a binding to the PostgreSQL instance: +* Create a S2S policy "Key Manager" between SM and the DB ``` -ibmcloud code-engine job update \ - --name csv-to-sql \ - --trusted-profiles-enabled true \ - --env COS_REGION=${REGION} \ - --env COS_TRUSTED_PROFILE_NAME=${COS_TRUSTED_PROFILE_NAME} \ - --env SM_TRUSTED_PROFILE_NAME=${SM_TRUSTED_PROFILE_NAME}\ - --env SM_SERVICE_URL=${SM_SERVICE_URL} \ - --env SM_PG_SECRET_ID=${SM_PG_SECRET_ID} +ibmcloud iam authorization-policy-create secrets-manager databases-for-postgresql \ + "Key Manager" \ + --source-service-instance-id $SM_INSTANCE_ID \ + --target-service-instance-id $DB_INSTANCE_ID ``` -Create a Secrets Manager instance -``` -ibmcloud resource service-instance-create credential-store secrets-manager 7713c3a8-3be8-4a9a-81bb-ee822fcaac3d eu-es -p \ -'{ - "allowed_network": "private-only" -}' +* Create the service credential to access the PostgreSQL instance ``` +SM_SECRET_FOR_PG_NAME=pg-access-credentials +ibmcloud secrets-manager secret-create --secret-type="service_credentials" --secret-name="$SM_SECRET_FOR_PG_NAME" --secret-source-service="{\"instance\": {\"crn\": \"$DB_INSTANCE_ID\"},\"parameters\": {},\"role\": {\"crn\": \"crn:v1:bluemix:public:iam::::serviceRole:Writer\"}}" -// Create a Trusted Profile for Secrets Manager +export SM_SECRET_FOR_PG_ID=$(ibmcloud sm secret-by-name --name $SM_SECRET_FOR_PG_NAME --secret-type service_credentials --secret-group-name default --output JSON|jq -r '.id') +``` -// Create a S2S policy "Key Manager" between SM and the DB -// Create a service credential for PG with automatic key rotation +* Create the Code Engine app: +``` +export CE_APP_NAME=csv-to-sql +export TRUSTED_PROFILE_FOR_COS_NAME=cos-to-sql--ce-to-cos-access +export TRUSTED_PROFILE_FOR_SM_NAME=cos-to-sql--ce-to-sm-access +ibmcloud code-engine app create \ + --name ${CE_APP_NAME} \ + --source ./ \ + --cpu 0.25 \ + --memory 0.5G \ + --trusted-profiles-enabled="true" \ + --probe-ready type=http \ + --probe-ready path=/readiness \ + --probe-ready interval=30 \ + --env COS_REGION=${REGION} \ + --env COS_TRUSTED_PROFILE_NAME=${TRUSTED_PROFILE_FOR_COS_NAME} \ + --env SM_TRUSTED_PROFILE_NAME=${TRUSTED_PROFILE_FOR_SM_NAME} \ + --env SM_SERVICE_URL=${SECRETS_MANAGER_URL} \ + --env SM_PG_SECRET_ID=${SM_SECRET_FOR_PG_ID} +``` +## Trusted Profile setup -// ibmcloud secrets-manager secret-by-name --secret-type service_credentials --name pg-credentials --secret-group-name --service-url https://4e61488a-b76f-4d44-ba0e-ed2489d8a57a.private.eu-es.secrets-manager.appdomain.cloud +* Create a trusted profile that grants a Code Engine app access to your COS bucket +``` +ibmcloud iam trusted-profile-create ${TRUSTED_PROFILE_FOR_COS_NAME} +ibmcloud iam trusted-profile-link-create ${TRUSTED_PROFILE_FOR_COS_NAME} --name ce-app-${CE_APP_NAME} --cr-type CE --link-crn ${CE_INSTANCE_ID} --link-component-type application --link-component-name ${CE_APP_NAME} +ibmcloud iam trusted-profile-policy-create ${TRUSTED_PROFILE_FOR_COS_NAME} --roles "Content Reader" --service-name cloud-object-storage --service-instance ${COS_INSTANCE_ID} --resource-type bucket --resource ${COS_BUCKET_NAME} +``` -// https://github.com/IBM/secrets-manager-node-sdk -Upload a CSV file to COS, to initate an event that leads to a job execution: +* Create the trusted profile to access Secrets Manager ``` -ibmcloud cos object-put \ - --bucket $BUCKET \ - --key users.csv \ - --body ./samples/users.csv \ - --content-type text/csv +ibmcloud iam trusted-profile-create ${TRUSTED_PROFILE_FOR_SM_NAME} +ibmcloud iam trusted-profile-link-create ${TRUSTED_PROFILE_FOR_SM_NAME} --name ce-app-${CE_APP_NAME} --cr-type CE --link-crn ${CE_INSTANCE_ID} --link-component-type application --link-component-name ${CE_APP_NAME} +ibmcloud iam trusted-profile-policy-create ${TRUSTED_PROFILE_FOR_SM_NAME} --roles "SecretsReader" --service-name secrets-manager --service-instance ${SM_INSTANCE_ID} ``` -List all jobs to determine the one, that processes the COS bucket update: +## Setting up eventing + +* Create an authorization policy to allow the Code Engine project receive events from COS: ``` -ibmcloud code-engine jobrun list \ - --job csv-to-sql \ - --sort-by age +ibmcloud iam authorization-policy-create codeengine cloud-object-storage \ + "Notifications Manager" \ + --source-service-instance-id ${CE_INSTANCE_ID} \ + --target-service-instance-id ${COS_INSTANCE_ID} ``` -Inspect the job execution by opening the logs: +* Create the subscription for all COS events: ``` -ibmcloud code-engine jobrun logs \ - --name +ibmcloud ce sub cos create \ + --name "coswatch-${CE_APP_NAME}" \ + --bucket ${COS_BUCKET_NAME} \ + --destination ${CE_APP_NAME} \ + --destination-type app \ + --path /cos-to-sql ``` -Or do the two commands in one, using this one-liner: +## Verify the solution + +* Upload a CSV file to COS, to initate an event that leads to a job execution: ``` -jobrunname=$(ibmcloud ce jr list -j csv-to-sql -s age -o json | jq -r '.items[0] | .metadata.name') && ibmcloud ce jr logs -n $jobrunname -f +ibmcloud cos object-put \ + --bucket ${COS_BUCKET_NAME} \ + --key users.csv \ + --body ./samples/users.csv \ + --content-type text/csv ``` - +* Inspect the app execution by opening the logs: +``` +ibmcloud code-engine app logs \ + --name ${CE_APP_NAME} \ + --follow ``` -kubectl patch jobdefinitions csv-to-sql --type='json' -p='[{"op": "add", "path": "/spec/template/mountComputeResourceToken", "value":true}]' -kubectl patch ksvc csv-to-sql-app --type='json' -p='[{"op": "add", "path": "/spec/template/metadata/annotations/codeengine.cloud.ibm.com~1mount-compute-resource-token", "value":"true"}]' -``` \ No newline at end of file diff --git a/cos-to-sql/app.mjs b/cos-to-sql/app.mjs new file mode 100644 index 00000000..c98740cf --- /dev/null +++ b/cos-to-sql/app.mjs @@ -0,0 +1,203 @@ +import { existsSync } from "fs"; +import express from "express"; +import favicon from "serve-favicon"; +import path from "path"; +import { fileURLToPath } from "url"; +const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file +const __dirname = path.dirname(__filename); // get the name of the directory + +// Libraries needed to materialize the authentication to COS and to SecretsManager +// and to read service credentials from SecretsManager +import SecretsManager from "@ibm-cloud/secrets-manager/secrets-manager/v2.js"; +import { ContainerAuthenticator } from "ibm-cloud-sdk-core"; + +// Imports to handle data base related operations +import { addUser, closeDBConnection, deleteUsers, getPgClient, listUsers } from "./utils/db.mjs"; +import { getObjectContent } from "./utils/cos.mjs"; +import { convertCsvToDataStruct } from "./utils/utils.mjs"; + +console.info("Starting the app ..."); + +const requiredEnvVars = [ + "COS_REGION", + "COS_TRUSTED_PROFILE_NAME", + "SM_TRUSTED_PROFILE_NAME", + "SM_SERVICE_URL", + "SM_PG_SECRET_ID", +]; + +requiredEnvVars.forEach((envVarName) => { + if (!process.env[envVarName]) { + console.log(`Failed to start app. Missing '${envVarName}' environment variable`); + process.exit(1); + } +}); + +// +// Initialize COS +const cosRegion = process.env.COS_REGION; +const cosTrustedProfileName = process.env.COS_TRUSTED_PROFILE_NAME; +let cosAuthenticator; +if (cosTrustedProfileName) { + // create an authenticator to access the COS instance based on a trusted profile + cosAuthenticator = new ContainerAuthenticator({ + iamProfileName: cosTrustedProfileName, + }); +} + +// +// Initialize Secrets Manager SDK +const smTrustedProfileName = process.env.SM_TRUSTED_PROFILE_NAME; +const smServiceURL = process.env.SM_SERVICE_URL; +const smPgSecretId = process.env.SM_PG_SECRET_ID; +let secretsManager; +if (smTrustedProfileName && smServiceURL) { + // create an authenticator to access the SecretsManager instance based on a trusted profile + const smAuthenticator = new ContainerAuthenticator({ + iamProfileName: smTrustedProfileName, + }); + // Create an instance of the SDK by providing an authentication mechanism and your Secrets Manager instance URL + secretsManager = new SecretsManager({ + authenticator: smAuthenticator, + serviceUrl: smServiceURL, + }); +} + +const app = express(); +app.use(express.json()); +app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))) + +// use router to bundle all routes to / +const router = express.Router(); +app.use("/", router); + +// +// Readiness endpoint +router.get("/readiness", (req, res) => { + console.log(`handling /readiness`); + if (!existsSync("/var/run/secrets/codeengine.cloud.ibm.com/compute-resource-token/token")) { + console.error("Mounting the trusted profile compute resource token is not enabled"); + res.writeHead(500, { "Content-Type": "application/json" }); + res.end('{"error": "Mounting the trusted profile compute resource token is not enabled"}'); + return; + } + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end('{"status": "ok"}'); + return; +}); + +// +// Ingestion endpoint +router.post("/cos-to-sql", async (req, res) => { + console.log(`handling /cos-to-sql for '${req.url}'`); + console.log(`request headers: '${JSON.stringify(req.headers)}`); + const event = req.body; + console.log(`request body: '${event}'`); + + // + // assess whether the request payload contains information about the COS file that got updated + if (!event) { + console.log("Request does not contain any event data"); + res.writeHead(400, { "Content-Type": "application/json" }); + res.end('{"error": "request does not contain any event data"}'); + return; + } + + // + // make sure that the event relates to a COS write operation + if (event.notification.event_type !== "Object:Write") { + console.log(`COS operation '${event.notification.event_type}' does not match expectations 'Object:Write'`); + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(`{"error": "COS operation '${event.notification.event_type}' does not match expectations 'Object:Write'"}`); + return; + } + if (event.notification.content_type !== "text/csv") { + console.log( + `COS update did happen on file '${event.key}' which is of type '${event.notification.content_type}' (expected type 'text/csv')` + ); + res.writeHead(400, { "Content-Type": "application/json" }); + res.end( + `{"error": "COS update did happen on file '${event.key}' which is of type '${event.notification.content_type}' (expected type 'text/csv')"}` + ); + return; + } + console.log(`Received a COS update event on the CSV file '${event.key}' in bucket '${event.bucket}'`); + + // + // Retrieve the COS object that got updated + console.log(`Retrieving file content of '${event.key}' from bucket ${event.bucket} ...`); + const fileContent = await getObjectContent(cosAuthenticator, cosRegion, event.bucket, event.key); + + // + // Convert CSV to a object structure representing an array of users + console.log(`Converting CSV data to a data struct ...`); + const users = await convertCsvToDataStruct(fileContent); + console.log(`users: ${JSON.stringify(users)}`); + + const pgClient = await getPgClient(secretsManager, smPgSecretId); + + // + // Perform a single SQL insert statement per user + console.log(`Writing converted CSV data to the PostgreSQL database ...`); + const insertOperations = []; + users.forEach((userToAdd) => { + insertOperations.push(addUser(pgClient, userToAdd.Firstname, userToAdd.Lastname)); + }); + + // Wait for all SQL insert operations to finish + console.log(`Waiting for all SQL INSERT operations to finish ...`); + await Promise.all(insertOperations) + .then((results) => { + results.forEach((result, idx) => console.log(`Added ${JSON.stringify(users[idx])} -> ${JSON.stringify(result)}`)); + console.info("COMPLETED"); + return Promise.resolve(); + }) + .catch((err) => { + console.error("Failed to add users to the database", err); + console.info("FAILED"); + return Promise.reject(); + }); + + console.log(`All ${insertOperations?.length} insertions done!`); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(`{"status": "done"}`); + return; +}); + +// +// Endpoint that drops the users table +router.get("/clear", async (req, res) => { + console.log(`handling /clear for '${req.url}'`); + const pgClient = await getPgClient(secretsManager, smPgSecretId); + await deleteUsers(pgClient); + console.log(`Deletions done!`); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(`{"status": "done"}`); + return; +}); + +// +// Default http endpoint, which prints the list of all users in the database +router.get("/", async (req, res) => { + console.log(`handling / for '${req.url}'`); + const pgClient = await getPgClient(secretsManager, smPgSecretId); + const allUsers = await listUsers(pgClient); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ users: allUsers.rows })); +}); + +// start server +const port = process.env.PORT || 8080; +const server = app.listen(port, () => { + console.log(`Server is up and running on port ${port}!`); +}); + +process.on("SIGTERM", async () => { + console.info("SIGTERM signal received."); + await closeDBConnection(); + + server.close(() => { + console.log("Http server closed."); + }); +}); diff --git a/cos-to-sql/package-lock.json b/cos-to-sql/package-lock.json index 6878eaca..fbb34d22 100644 --- a/cos-to-sql/package-lock.json +++ b/cos-to-sql/package-lock.json @@ -1,762 +1,1533 @@ { - "name": "ce-cos-to-sql", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "ce-cos-to-sql", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@ibm-cloud/secrets-manager": "^2.0.9", - "csv-parser": "^3.2.0", - "ibm-cloud-sdk-core": "^5.3.0", - "pg": "^8.14.0", - "pg-connection-string": "^2.7.0" - } - }, - "node_modules/@ibm-cloud/secrets-manager": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@ibm-cloud/secrets-manager/-/secrets-manager-2.0.9.tgz", - "integrity": "sha512-5q6m1eHA+ZQ+J70++i41ASgDO+cPCVfmhv/WRspWyKfH4BmC47mm7+hl3fcGc6+JjoNjuTbn4i9O7XjIIdZ/CA==", - "dependencies": { - "extend": "^3.0.2", - "ibm-cloud-sdk-core": "^5.1.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" - }, - "node_modules/@types/node": { - "version": "18.19.80", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.80.tgz", - "integrity": "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", - "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/csv-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz", - "integrity": "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==", - "bin": { - "csv-parser": "bin/csv-parser" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/file-type": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", - "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", - "dependencies": { - "readable-web-to-node-stream": "^3.0.0", - "strtok3": "^6.2.4", - "token-types": "^4.1.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/ibm-cloud-sdk-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ibm-cloud-sdk-core/-/ibm-cloud-sdk-core-5.3.2.tgz", - "integrity": "sha512-YhtS+7hGNO61h/4jNShHxbbuJ1TnDqiFKQzfEaqePnonOvv8NnxWxOk92FlKKCCzZNOT34Gnd7WCLVJTntwEFQ==", - "dependencies": { - "@types/debug": "^4.1.12", - "@types/node": "^18.19.80", - "@types/tough-cookie": "^4.0.0", - "axios": "^1.8.2", - "camelcase": "^6.3.0", - "debug": "^4.3.4", - "dotenv": "^16.4.5", - "extend": "3.0.2", - "file-type": "16.5.4", - "form-data": "4.0.0", - "isstream": "0.1.2", - "jsonwebtoken": "^9.0.2", - "mime-types": "2.1.35", - "retry-axios": "^2.6.0", - "tough-cookie": "^4.1.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" - }, - "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "dependencies": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/peek-readable": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", - "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/pg": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.14.0.tgz", - "integrity": "sha512-nXbVpyoaXVmdqlKEzToFf37qzyeeh7mbiXsnoWvstSqohj88yaa/I/Rq/HEVn2QPSZEuLIJa/jSpRDyzjEx4FQ==", - "dependencies": { - "pg-connection-string": "^2.7.0", - "pg-pool": "^3.8.0", - "pg-protocol": "^1.8.0", - "pg-types": "^2.1.0", - "pgpass": "1.x" - }, - "engines": { - "node": ">= 8.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.1.1" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", - "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", - "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.8.0.tgz", - "integrity": "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz", - "integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "dependencies": { - "split2": "^4.1.0" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "dependencies": { - "punycode": "^2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, - "node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/readable-stream/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", - "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", - "dependencies": { - "readable-stream": "^4.7.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, - "node_modules/retry-axios": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-2.6.0.tgz", - "integrity": "sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ==", - "engines": { - "node": ">=10.7.0" - }, - "peerDependencies": { - "axios": "*" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strtok3": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", - "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^4.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/token-types": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", - "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } + "name": "ce-cos-to-sql", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ce-cos-to-sql", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@ibm-cloud/secrets-manager": "^2.0.9", + "csv-parser": "^3.2.0", + "express": "^4.21.2", + "ibm-cloud-sdk-core": "^5.3.0", + "pg": "^8.14.0", + "pg-connection-string": "^2.7.0", + "serve-favicon": "^2.5.0" + } + }, + "node_modules/@ibm-cloud/secrets-manager": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@ibm-cloud/secrets-manager/-/secrets-manager-2.0.9.tgz", + "integrity": "sha512-5q6m1eHA+ZQ+J70++i41ASgDO+cPCVfmhv/WRspWyKfH4BmC47mm7+hl3fcGc6+JjoNjuTbn4i9O7XjIIdZ/CA==", + "dependencies": { + "extend": "^3.0.2", + "ibm-cloud-sdk-core": "^5.1.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, + "node_modules/@types/node": { + "version": "18.19.80", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.80.tgz", + "integrity": "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "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.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/csv-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz", + "integrity": "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==", + "bin": { + "csv-parser": "bin/csv-parser" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ibm-cloud-sdk-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ibm-cloud-sdk-core/-/ibm-cloud-sdk-core-5.3.2.tgz", + "integrity": "sha512-YhtS+7hGNO61h/4jNShHxbbuJ1TnDqiFKQzfEaqePnonOvv8NnxWxOk92FlKKCCzZNOT34Gnd7WCLVJTntwEFQ==", + "dependencies": { + "@types/debug": "^4.1.12", + "@types/node": "^18.19.80", + "@types/tough-cookie": "^4.0.0", + "axios": "^1.8.2", + "camelcase": "^6.3.0", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "extend": "3.0.2", + "file-type": "16.5.4", + "form-data": "4.0.0", + "isstream": "0.1.2", + "jsonwebtoken": "^9.0.2", + "mime-types": "2.1.35", + "retry-axios": "^2.6.0", + "tough-cookie": "^4.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/pg": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.14.0.tgz", + "integrity": "sha512-nXbVpyoaXVmdqlKEzToFf37qzyeeh7mbiXsnoWvstSqohj88yaa/I/Rq/HEVn2QPSZEuLIJa/jSpRDyzjEx4FQ==", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.8.0", + "pg-protocol": "^1.8.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.8.0.tgz", + "integrity": "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz", + "integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readable-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", + "dependencies": { + "readable-stream": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/retry-axios": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-2.6.0.tgz", + "integrity": "sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ==", + "engines": { + "node": ">=10.7.0" + }, + "peerDependencies": { + "axios": "*" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-favicon": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/serve-favicon/-/serve-favicon-2.5.0.tgz", + "integrity": "sha512-FMW2RvqNr03x+C0WxTyu6sOv21oOjkq5j8tjquWccwa6ScNyGFOGJVpuS1NmTVGBAHS07xnSKotgf2ehQmf9iA==", + "dependencies": { + "etag": "~1.8.1", + "fresh": "0.5.2", + "ms": "2.1.1", + "parseurl": "~1.3.2", + "safe-buffer": "5.1.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-favicon/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "node_modules/serve-favicon/node_modules/safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } } + } } diff --git a/cos-to-sql/package.json b/cos-to-sql/package.json index ef185399..7b2692a2 100644 --- a/cos-to-sql/package.json +++ b/cos-to-sql/package.json @@ -1,26 +1,28 @@ { - "name": "ce-cos-to-sql", - "version": "1.0.0", - "description": "", - "main": "job.mjs", - "scripts": { - "start": "node .", - "local": "node ./src/job.mjs", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [ - "code-engine", - "cos", - "cloud-object-storage", - "csv" - ], - "license": "MIT", - "homepage": "https://cloud.ibm.com/containers/serverless", - "dependencies": { - "@ibm-cloud/secrets-manager": "^2.0.9", - "csv-parser": "^3.2.0", - "ibm-cloud-sdk-core": "^5.3.0", - "pg": "^8.14.0", - "pg-connection-string": "^2.7.0" - } + "name": "ce-cos-to-sql", + "version": "1.0.0", + "description": "", + "main": "app.mjs", + "scripts": { + "start": "node .", + "local": "node ./src/job.mjs", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "code-engine", + "cos", + "cloud-object-storage", + "csv" + ], + "license": "MIT", + "homepage": "https://cloud.ibm.com/containers/serverless", + "dependencies": { + "@ibm-cloud/secrets-manager": "^2.0.9", + "csv-parser": "^3.2.0", + "express": "^4.21.2", + "ibm-cloud-sdk-core": "^5.3.0", + "pg": "^8.14.0", + "pg-connection-string": "^2.7.0", + "serve-favicon": "^2.5.0" + } } diff --git a/cos-to-sql/public/favicon.ico b/cos-to-sql/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8f688bed87fc160ec873ed29f827dab6ef62b71c GIT binary patch literal 2247 zcmV;&2srnNP)&H!#vjgP;#EPD z`C&5ANgRF%Iywquq7e;_8m*5J#TEsomPcP;q4ojN_TKi~d-h&yew1R7i*0Y)flOG* zP4+ow?Y+Nm@3p_Z_P)SbJB5kR4c&FWTh>)~`*?Pv#;f7wXbrQ)Lk!$Io&=onYB<@Z z2pTMky1zEnb8QhJ>8QhUL%XvL$yYziXP_4>7AJeB*m8`k&)QVB4iL zUh})+Z%{DjmW5yJzxIcGQ+0Jows@PF-W#gjNdDxzoN)dEn@m2t*}Y)<aH;zT$qa{G|KR8&aUG`3KyP@HZ!uN-&c;UYePS>vfi_Xrv ziM@D2X{>{?FXi_6UshE_z4UU~6!PMJRJ-aT5}FC(xCU>F_w;;OQ(o>5)#nXIci#fd zKXOF>X>;y*ohK4_zHC20O%&_hUweJ=Uq_R%Uf$4B z0zWw!jP_vr*UeQG6=vY`m96^<1A>Q@tM1joKtTND1}&+_wiL|$Dy@v}Us3Q$)84$L zHr~BL7@az|;E6sXO9#G}ymoLpiMv6>$@lxbJ4OW_0ElF3>GXPVa5o$Aj!c{wJV9Vn zSm{oh{DAoQ(vSBodwXKpZG*w5-v?sEO?OUwF_oZ&rYC-@kzoH$x=}PG-DeQUj`|7j zzCAY6OhH-hkyV7Q^#l{ihE6Vp4|Sh0cF#{}g2cB_bvZBy~wGEkSkek?L~#PC{d%7GB>} zsIqi9gnz;!5BRZ|JO*L+x7`gKM#aL%(wo*+$ksDQKVz+E}DnZTdD`OqY#Y_TS` zNu*lpmz8x7nP%74W_u>If;6onl0z!s{PyG7Ga83SASMpb>=b2%wTcAS6Nx?4o_{A| zWJU^p>gGd{u$2Ng~XW`=T*_@(I3o4v(~f~O$Z*H zHX(09DCN2ZY{?O;w`Kw!F;S7Ogd?eNcipG!+eQi=0JhGX`6@F#C6+Ev_Q->$mQfMP zU(<78Ku69KL;7f>A>lv=#ZtFnyDy{AFRsyEdxzh6Ykm)6G0t@Hhrexp&RVJxrn?vK zsJ}2RfxO3iFBL20VpMOJ&D)P(m;3CKW<#AU!H$f2ez%cZwzj13y)=5HR#j191nL>K zlxJA4IEg^`kG08CpPBH3+ z1=WWlB0|Rr5Ifhir70%ez!k_OiEqA7P$gCm@c`aUTO1tjn}_7oaPzu z5f;`Uc9PjMO8Wtcm3e?BB9%7Z=*h|HhSz0rxLyM|tuw(wQTP@Bf794kBdVVj)mnor zPMVzbn-Qgu=;*~asnfm=*x(9Iafe5`VflguRsn!zy;m%`!ZhcUHJ^UT2WstD-DG5# zPt6f=udr5qw6|hvXN-vZWz@$W5$d#-mI87LfHB6VNu- z??O}seub-s(Dc(1M$wIJ*NV=%+a!8F$UdiN%gL#P###WqV64U}nS?XQqR+Qoq!3)k zJ|_kG!J0wmhwliirQHxb37{Mn&ta7mp9$b+K6iL20^uzJXR7s*_tgKiaQhFO2ZXQ^ zk3jGT0AE4yI2dnYJ$e6Y;ODOGnJb#_B}e~;6nd3S+|zVd(F?;nk=-pT0Xz!AQ~=u{ zsDi?7FcQ!JlqC%SN<(W4=ws8Vaja~i6^_5x8i3|O<$+s=L3POzX$mWiC=?cxrYm(; zwn8%gks$f|mX(vXjqFgrN=jg{3cyVul))L~tpHL0>3+~O@Jxfsnf^ooljv4jeJBpE zZ>`?*I@X^0+Hq)2|E1)>K^ZL01rRx=MTBD`snx(Jr<;jmql{y|$KP{*8H3xfP>%B8 zpcTRm?e}L7>%W4b35XCL?GGFrKH0xCU7fN_BflPb!BSq{3LA?NEW literal 0 HcmV?d00001 diff --git a/cos-to-sql/src/app.mjs b/cos-to-sql/src/app.mjs deleted file mode 100644 index 2684aff0..00000000 --- a/cos-to-sql/src/app.mjs +++ /dev/null @@ -1,408 +0,0 @@ -import { existsSync } from "fs"; -import http from "http"; - -// Libraries needed to materialize the authentication to COS and to SecretsManager -// and to read service credentials from SecretsManager -import SecretsManager from "@ibm-cloud/secrets-manager/secrets-manager/v2.js"; -import { ContainerAuthenticator } from "ibm-cloud-sdk-core"; - -// Libraries to convert CSV content into a object structure -import csv from "csv-parser"; -import { Readable } from "stream"; - -// Libraries to access PostgreSQL -import pg from "pg"; -import pgConnectionString from "pg-connection-string"; - -console.info("Starting the app ..."); - -// -// Initialize COS -const cosRegion = process.env.COS_REGION; -const cosTrustedProfileName = process.env.COS_TRUSTED_PROFILE_NAME; -let cosAuthenticator; -if (cosTrustedProfileName) { - // create an authenticator to access the COS instance based on a trusted profile - cosAuthenticator = new ContainerAuthenticator({ - iamProfileName: cosTrustedProfileName, - }); -} - -// -// Initialize Secrets Manager SDK -const smTrustedProfileName = process.env.SM_TRUSTED_PROFILE_NAME; -const smServiceURL = process.env.SM_SERVICE_URL; -const smPgSecretId = process.env.SM_PG_SECRET_ID; -let secretsManager; -if (smTrustedProfileName && smServiceURL) { - // create an authenticator to access the SecretsManager instance based on a trusted profile - const smAuthenticator = new ContainerAuthenticator({ - iamProfileName: smTrustedProfileName, - }); - // Create an instance of the SDK by providing an authentication mechanism and your Secrets Manager instance URL - secretsManager = new SecretsManager({ - authenticator: smAuthenticator, - serviceUrl: smServiceURL, - }); -} - -let _pgClient; - -const server = http - .createServer(async function (request, response) { - // - // Readiness endpoint - if (request.url == "/readiness") { - if (!cosRegion) { - console.error("environment variable COS_REGION is not set"); - response.writeHead(500, { "Content-Type": "application/json" }); - response.end('{"error": "environment variable COS_REGION is not set"}'); - return; - } - - if (!cosTrustedProfileName) { - console.error("environment variable COS_TRUSTED_PROFILE_NAME is not set"); - response.writeHead(500, { "Content-Type": "application/json" }); - response.end('{"error": "environment variable COS_TRUSTED_PROFILE_NAME is not set"}'); - return; - } - - if (!smTrustedProfileName) { - console.error("environment variable SM_TRUSTED_PROFILE_NAME is not set"); - response.writeHead(500, { "Content-Type": "application/json" }); - response.end('{"error": "environment variable SM_TRUSTED_PROFILE_NAME is not set"}'); - return; - } - - if (!smServiceURL) { - console.error("environment variable SM_SERVICE_URL is not set"); - response.writeHead(500, { "Content-Type": "application/json" }); - response.end('{"error": "environment variable SM_SERVICE_URL is not set"}'); - return; - } - - if (!smPgSecretId) { - console.error("environment variable SM_PG_SECRET_ID is not set"); - response.writeHead(500, { "Content-Type": "application/json" }); - response.end('{"error": "environment variable SM_PG_SECRET_ID is not set"}'); - return; - } - - if (!existsSync("/var/run/secrets/codeengine.cloud.ibm.com/compute-resource-token/token")) { - console.error("Mounting the trusted profile compute resource token is not enabled"); - response.writeHead(500, { "Content-Type": "application/json" }); - response.end('{"error": "Mounting the trusted profile compute resource token is not enabled"}'); - return; - } - - response.writeHead(200, { "Content-Type": "application/json" }); - response.end('{"status": "ok"}'); - return; - } - - // - // Ingestion endpoint - if (request.url == "/cos-to-sql") { - const body = await getBody(request); - console.log(`request body: '${body}'`); - console.log(`request headers: '${JSON.stringify(request.headers)}`); - - // - // assess whether the jobrun execution contains information about the COS file that got updated - if (!body) { - console.log("Request does not contain any event data"); - response.writeHead(400, { "Content-Type": "application/json" }); - response.end('{"error": "request does not contain any event data"}'); - return; - } - const eventData = JSON.parse(body); - console.log(`eventData: '${JSON.stringify(eventData)}'`); - - // - // make sure that the event relates to a COS write operation - if (eventData.notification.event_type !== "Object:Write") { - console.log(`COS operation '${eventData.notification.event_type}' does not match expectations 'Object:Write'`); - response.writeHead(400, { "Content-Type": "application/json" }); - response.end( - `{"error": "COS operation '${eventData.notification.event_type}' does not match expectations 'Object:Write'"}` - ); - return; - } - if (eventData.notification.content_type !== "text/csv") { - console.log( - `COS update did happen on file '${eventData.key}' which is of type '${eventData.notification.content_type}' (expected type 'text/csv')` - ); - response.writeHead(400, { "Content-Type": "application/json" }); - response.end( - `{"error": "COS update did happen on file '${eventData.key}' which is of type '${eventData.notification.content_type}' (expected type 'text/csv')"}` - ); - return; - } - console.log(`Received a COS update event on the CSV file '${eventData.key}' in bucket '${eventData.bucket}'`); - - // - // retrieve the COS object that got updated - console.log(`Retrieving file content of '${eventData.key}' from bucket ${eventData.bucket} ...`); - const fileContent = await getObjectContent(cosAuthenticator, cosRegion, eventData.bucket, eventData.key); - - // - // convert CSV to a object structure - console.log(`Converting CSV data to a data struct ...`); - const users = await convertCsvToDataStruct(fileContent); - console.log(`users: ${JSON.stringify(users)}`); - - const pgClient = await getPgClient(secretsManager, smPgSecretId); - - // Do something meaningful with the data - // https://github.com/IBM-Cloud/compose-postgresql-helloworld-nodejs/blob/master/server.js - console.log(`Writing converted CSV data to the PostgreSQL database ...`); - const insertOperations = []; - users.forEach((userToAdd) => { - insertOperations.push(addUser(pgClient, userToAdd.Firstname, userToAdd.Lastname)); - }); - - // Wait for all SQL insert operations to finish - console.log(`Waiting for all SQL INSERT operations to finish ...`); - await Promise.all(insertOperations) - .then((results) => { - results.forEach((result, idx) => - console.log(`Added ${JSON.stringify(users[idx])} -> ${JSON.stringify(result)}`) - ); - console.info("COMPLETED"); - return Promise.resolve(); - }) - .catch((err) => { - console.error("Failed to add users to the database", err); - console.info("FAILED"); - return Promise.reject(); - }); - - console.log(`Insertions done!`); - response.writeHead(200, { "Content-Type": "application/json" }); - response.end(`{"status": "done"}`); - return; - } - - // - // Endpoint that drops the users table - if (request.url == "/clear") { - const pgClient = await getPgClient(secretsManager, smPgSecretId); - await deleteUsers(pgClient); - console.log(`Deletions done!`); - response.writeHead(200, { "Content-Type": "application/json" }); - response.end(`{"status": "done"}`); - return; - } - - // - // Default http endpoint, which prints a simple hello world - const pgClient = await getPgClient(secretsManager, smPgSecretId); - const allUsers = await listUsers(pgClient); - response.writeHead(200, { "Content-Type": "application/json" }); - response.end(JSON.stringify({ users: allUsers.rows })); - }) - .listen(8080); - -console.log("Server running at http://0.0.0.0:8080/"); - -process.on("SIGTERM", () => { - console.info("SIGTERM signal received."); - server.close(() => { - console.log("Http server closed."); - - if (_pgClient) { - _pgClient.end(); - console.log("PG client ended."); - } - }); -}); - -async function getObjectContent(authenticator, region, bucket, key) { - const fn = "getObjectContent "; - const startTime = Date.now(); - console.log(`${fn} > region: '${region}', bucket: '${bucket}', key: '${key}'`); - - // prepare the request to download the content of a file - const requestOptions = { - method: "GET", - }; - - // authenticate the request - await authenticator.authenticate(requestOptions); - - // perform the request - const response = await fetch( - `https://s3.direct.${region}.cloud-object-storage.appdomain.cloud/${bucket}/${key}`, - requestOptions - ); - - if (response.status !== 200) { - const err = new Error(`Unexpected status code: ${response.status}`); - console.log(`${fn} < failed - error: ${err.message}; duration ${Date.now() - startTime} ms`); - return Promise.reject(err); - } - - // read the response - const responseBody = await response.text(); - - console.log(`${fn} < done - duration ${Date.now() - startTime} ms`); - return responseBody; -} - -function convertCsvToDataStruct(csvContent) { - return new Promise((resolve) => { - // the result to return - const results = []; - - // create a new readable stream - var readableStream = new Readable(); - - // the CSV parser consumes the stream - readableStream - .pipe(csv()) - .on("data", (data) => results.push(data)) - .on("end", () => { - console.log(`converted CSV data: ${JSON.stringify(results)}`); - - resolve(results); - }); - - // push the CSV file content to the stream - readableStream.push(csvContent); - readableStream.push(null); // indicates end-of-file - }); -} - -function connectDb(connectionString, caCert) { - return new Promise((resolve, reject) => { - const postgreConfig = pgConnectionString.parse(connectionString); - - // Add some ssl - postgreConfig.ssl = { - ca: caCert, - }; - - // set up a new client using our config details - let client = new pg.Client(postgreConfig); - - client.connect((err) => { - if (err) { - console.error(`Failed to connect to postgreSQL host '${postgreConfig.host}'`, err); - return reject(err); - } - - client.query( - "CREATE TABLE IF NOT EXISTS users (firstname varchar(256) NOT NULL, lastname varchar(256) NOT NULL)", - (err, result) => { - if (err) { - console.log(`Failed to create PostgreSQL table 'users'`, err); - return reject(err); - } - console.log( - `Established PostgreSQL client connection to '${postgreConfig.host}' - user table init: ${JSON.stringify( - result - )}` - ); - return resolve(client); - } - ); - }); - }); -} - -async function getPgClient(secretsManager, secretId) { - const fn = "getPgClient "; - const startTime = Date.now(); - console.log(`${fn} >`); - - // Check whether the pg client had been initialized already - if (_pgClient) { - console.log(`${fn} < from local cache`); - return Promise.resolve(_pgClient); - } - - console.log(`Fetching secret '${secretId}' ...`); - // Use the Secrets Manager API to get the secret using the secret ID - const res = await secretsManager.getSecret({ - id: secretId, - }); - console.log(`Secret '${secretId}' fetched in ${Date.now() - startTime} ms`); - - // - // Connect to PostgreSQL - // https://node-postgres.com/ - console.log( - `Establishing connection to PostgreSQL database using SM secret '${res.result.name}' (last updated: '${res.result.updated_at}') ...` - ); - const pgCaCert = Buffer.from(res.result.credentials.connection.postgres.certificate.certificate_base64, "base64"); - const pgConnectionString = res.result.credentials.connection.postgres.composed[0]; - _pgClient = await connectDb(pgConnectionString, pgCaCert); - - console.log(`${fn} < done - duration ${Date.now() - startTime} ms`); - return _pgClient; -} - -function addUser(client, firstName, lastName) { - const fn = "addUser "; - const startTime = Date.now(); - console.log(`${fn} > firstName: '${firstName}', lastName: '${lastName}'`); - return new Promise(function (resolve, reject) { - const queryText = "INSERT INTO users(firstname,lastname) VALUES($1, $2)"; - client.query(queryText, [firstName, lastName], function (error, result) { - if (error) { - console.log(`${fn} < failed - error: ${error}; duration ${Date.now() - startTime} ms`); - return reject(error); - } - console.log(`${fn} < succeeded - duration ${Date.now() - startTime} ms`); - return resolve(result); - }); - }); -} - -function listUsers(client) { - const fn = "listUsers "; - const startTime = Date.now(); - console.log(`${fn} >`); - return new Promise(function (resolve, reject) { - const queryText = "SELECT * FROM users"; - client.query(queryText, undefined, function (error, result) { - if (error) { - console.log(`${fn} < failed - error: ${error}; duration ${Date.now() - startTime} ms`); - return reject(error); - } - console.log(`${fn} < succeeded - duration ${Date.now() - startTime} ms`); - return resolve(result); - }); - }); -} -function deleteUsers(client) { - const fn = "deleteUsers "; - const startTime = Date.now(); - console.log(`${fn} >`); - return new Promise(function (resolve, reject) { - const queryText = "DELETE FROM users"; - client.query(queryText, undefined, function (error, result) { - if (error) { - console.log(`${fn} < failed - error: ${error}; duration ${Date.now() - startTime} ms`); - return reject(error); - } - console.log(`${fn} < succeeded - duration ${Date.now() - startTime} ms`); - return resolve(result); - }); - }); -} - -function getBody(request) { - return new Promise((resolve) => { - const bodyParts = []; - let body; - request - .on("data", (chunk) => { - bodyParts.push(chunk); - }) - .on("end", () => { - body = Buffer.concat(bodyParts).toString(); - resolve(body); - }); - }); -} diff --git a/cos-to-sql/src/job.mjs b/cos-to-sql/src/job.mjs deleted file mode 100644 index e80085c6..00000000 --- a/cos-to-sql/src/job.mjs +++ /dev/null @@ -1,231 +0,0 @@ -// library needed to materialize the authentication to COS and to SecretsManager -// and to read service credentials from SecretsManager -import { ContainerAuthenticator } from "ibm-cloud-sdk-core"; -import SecretsManager from "@ibm-cloud/secrets-manager/secrets-manager/v2.js"; - -// library to convert CSV content into a object structure -import csv from "csv-parser"; -import { Readable } from "stream"; - -// library to access PostgreSQL -import pg from "pg"; -import pgConnectionString from "pg-connection-string"; - -console.info("Starting CSV to SQL conversion ..."); - -const run = async () => { - // - // assess whether the jobrun execution contains information about the COS file that got updated - if (!process.env.CE_DATA) { - console.log("< ABORT - job does not contain any event data"); - return process.exit(1); - } - const eventData = JSON.parse(process.env.CE_DATA); - console.log(`eventData: '${JSON.stringify(eventData)}'`); - - // - // make sure that the event relates to a COS write operation - if (eventData.operation !== "Object:Write") { - console.log(`< ABORT - COS operation '${eventData.operation}' does not match expectations 'Object:Write'`); - return process.exit(1); - } - if (eventData.notification.content_type !== "text/csv") { - console.log( - `< ABORT - COS update did happen on file '${eventData.key}' which is of type '${eventData.notification.content_type}' (expected type 'text/csv')` - ); - return process.exit(1); - } - console.log(`Received a COS update event on the CSV file '${eventData.key}' in bucket '${eventData.bucket}'`); - - const cosRegion = process.env.COS_REGION; - if (!cosRegion) { - console.error("environment variable COS_REGION is not set"); - process.exit(1); - } - - const cosTrustedProfileName = process.env.COS_TRUSTED_PROFILE_NAME; - if (!cosTrustedProfileName) { - console.error("environment variable COS_TRUSTED_PROFILE_NAME is not set"); - process.exit(1); - } - - // create an authenticator to access the COS instance based on a trusted profile - const cosAuthenticator = new ContainerAuthenticator({ - iamProfileName: cosTrustedProfileName, - }); - - // - // retrieve the COS object that got updated - console.log(`Retrieving file content of '${eventData.key}' from bucket ${eventData.bucket} ...`); - const fileContent = await getObjectContent(cosAuthenticator, cosRegion, eventData.bucket, eventData.key); - - // - // convert CSV to a object structure - console.log(`Converting CSV data to a data struct ...`); - const users = await convertCsvToDataStruct(fileContent); - console.log(`users: ${JSON.stringify(users)}`); - - const smTrustedProfileName = process.env.SM_TRUSTED_PROFILE_NAME; - if (!smTrustedProfileName) { - console.error("environment variable SM_TRUSTED_PROFILE_NAME is not set"); - process.exit(1); - } - - const smServiceURL = process.env.SM_SERVICE_URL; - if (!smServiceURL) { - console.error("environment variable SM_SERVICE_URL is not set"); - process.exit(1); - } - - // create an authenticator to access the SecretsManager instance based on a trusted profile - const smAuthenticator = new ContainerAuthenticator({ - iamProfileName: smTrustedProfileName, - }); - - // Create an instance of the SDK by providing an authentication mechanism and your Secrets Manager instance URL - const secretsManager = new SecretsManager({ - authenticator: smAuthenticator, - serviceUrl: smServiceURL, - }); - - const smPgSecretId = process.env.SM_PG_SECRET_ID; - if (!smPgSecretId) { - console.error("environment variable SM_PG_SECRET_ID is not set"); - process.exit(1); - } - - // Use the Secrets Manager API to get the secret using the secret ID - const res = await secretsManager.getSecret({ - id: smPgSecretId, - }); - - console.log(`Secret '${smPgSecretId}' content: '${JSON.stringify(res.result)}'`); - - // - // Connect to PostgreSQL - // https://node-postgres.com/ - console.log(`Establishing connection to PostgreSQL database using SM secret '${res.result.name}' (last updated: '${res.result.updated_at}') ...`); - const pgCaCert = Buffer.from(res.result.credentials.connection.postgres.certificate.certificate_base64, "base64"); - const pgConnectionString = res.result.credentials.connection.postgres.composed[0]; - const pgClient = await connectDb(pgConnectionString, pgCaCert); - - // Do something meaningful with the data - // https://github.com/IBM-Cloud/compose-postgresql-helloworld-nodejs/blob/master/server.js - console.log(`Writing converted CSV data to the PostgreSQL database ...`); - const insertOperations = []; - users.forEach((userToAdd) => { - insertOperations.push(addUser(pgClient, userToAdd.Firstname, userToAdd.Lastname)); - }); - - // Wait for all SQL insert operations to finish - console.log(`Waiting for all SQL INSERT operations to finish ...`); - Promise.all(insertOperations) - .then((results) => { - results.forEach((result, idx) => console.log(`Added ${JSON.stringify(users[idx])} -> ${JSON.stringify(result)}`)); - console.info("COMPLETED"); - }) - .catch((err) => { - console.error("Failed to add users to the database", err); - console.info("FAILED"); - }); -}; -run(); - -async function getObjectContent(authenticator, region, bucket, key) { - // prepare the request to download the content of a file - const requestOptions = { - method: "GET", - }; - - // authenticate the request - await authenticator.authenticate(requestOptions); - - // perform the request - const response = await fetch( - `https://s3.direct.${region}.cloud-object-storage.appdomain.cloud/${bucket}/${key}`, - requestOptions - ); - - if (response.status !== 200) { - console.error(`Unexpected status code: ${response.status}`); - process.exit(1); - } - - // read the response - const responseBody = await response.text(); - - return responseBody; -} - -function convertCsvToDataStruct(csvContent) { - return new Promise((resolve) => { - // the result to return - const results = []; - - // create a new readable stream - var readableStream = new Readable(); - - // the CSV parser consumes the stream - readableStream - .pipe(csv()) - .on("data", (data) => results.push(data)) - .on("end", () => { - console.log(`converted CSV data: ${JSON.stringify(results)}`); - - resolve(results); - }); - - // push the CSV file content to the stream - readableStream.push(csvContent); - readableStream.push(null); // indicates end-of-file - }); -} - -function connectDb(connectionString, caCert) { - return new Promise((resolve, reject) => { - const postgreConfig = pgConnectionString.parse(connectionString); - - // Add some ssl - postgreConfig.ssl = { - ca: caCert, - }; - - // set up a new client using our config details - let client = new pg.Client(postgreConfig); - - client.connect((err) => { - if (err) { - console.error(`Failed to connect to postgreSQL host '${postgreConfig.host}'`, err); - return reject(err); - } - - client.query( - "CREATE TABLE IF NOT EXISTS users (firstname varchar(256) NOT NULL, lastname varchar(256) NOT NULL)", - (err, result) => { - if (err) { - console.log(`Failed to create PostgreSQL table 'users'`, err); - return reject(err); - } - console.log( - `Established PostgreSQL client connection to '${postgreConfig.host}' - user table init: ${JSON.stringify( - result - )}` - ); - return resolve(client); - } - ); - }); - }); -} - -function addUser(client, firstName, lastName) { - return new Promise(function (resolve, reject) { - const queryText = "INSERT INTO users(firstname,lastname) VALUES($1, $2)"; - client.query(queryText, [firstName, lastName], function (error, result) { - if (error) { - return reject(error); - } - return resolve(result); - }); - }); -} diff --git a/cos-to-sql/utils/cos.mjs b/cos-to-sql/utils/cos.mjs new file mode 100644 index 00000000..7f04d093 --- /dev/null +++ b/cos-to-sql/utils/cos.mjs @@ -0,0 +1,31 @@ +export async function getObjectContent(authenticator, region, bucket, key) { + const fn = "getObjectContent "; + const startTime = Date.now(); + console.log(`${fn} > region: '${region}', bucket: '${bucket}', key: '${key}'`); + + // prepare the request to download the content of a file + const requestOptions = { + method: "GET", + }; + + // authenticate the request + await authenticator.authenticate(requestOptions); + + // perform the request + const response = await fetch( + `https://s3.direct.${region}.cloud-object-storage.appdomain.cloud/${bucket}/${key}`, + requestOptions + ); + + if (response.status !== 200) { + const err = new Error(`Unexpected status code: ${response.status}`); + console.log(`${fn} < failed - error: ${err.message}; duration ${Date.now() - startTime} ms`); + return Promise.reject(err); + } + + // read the response + const responseBody = await response.text(); + + console.log(`${fn} < done - duration ${Date.now() - startTime} ms`); + return responseBody; +} diff --git a/cos-to-sql/utils/db.mjs b/cos-to-sql/utils/db.mjs new file mode 100644 index 00000000..1b4398b7 --- /dev/null +++ b/cos-to-sql/utils/db.mjs @@ -0,0 +1,136 @@ +// Libraries to access PostgreSQL +import pg from "pg"; +const { Pool } = pg; +import pgConnectionString from "pg-connection-string"; + +// File that contains all data base related functionalities, like establishing a connection + +let _pgPool; + +function connectDb(connectionString, caCert) { + return new Promise((resolve, reject) => { + const postgreConfig = pgConnectionString.parse(connectionString); + + // Add some ssl + postgreConfig.ssl = { + ca: caCert, + }; + + // set up a new client using our config details + let client = new Pool(postgreConfig); + + client.connect((err) => { + if (err) { + console.error(`Failed to connect to postgreSQL host '${postgreConfig.host}'`, err); + return reject(err); + } + + client.query( + "CREATE TABLE IF NOT EXISTS users (firstname varchar(256) NOT NULL, lastname varchar(256) NOT NULL)", + (err, result) => { + if (err) { + console.log(`Failed to create PostgreSQL table 'users'`, err); + return reject(err); + } + console.log( + `Established PostgreSQL client connection to '${postgreConfig.host}' - user table init: ${JSON.stringify( + result + )}` + ); + return resolve(client); + } + ); + }); + }); +} + +export async function closeDBConnection() { + if (_pgPool) { + console.log("Draining PG pool."); + await _pgPool.end(); + console.log("PG pool has drained."); + } + Promise.resolve(); +} + +export async function getPgClient(secretsManager, secretId) { + const fn = "getPgClient "; + const startTime = Date.now(); + console.log(`${fn} >`); + + // Check whether the pg pool had been initialized already + if (_pgPool) { + console.log(`${fn} < from local cache`); + return Promise.resolve(_pgPool); + } + + console.log(`Fetching secret '${secretId}' ...`); + // Use the Secrets Manager API to get the secret using the secret ID + const res = await secretsManager.getSecret({ + id: secretId, + }); + console.log(`Secret '${secretId}' fetched in ${Date.now() - startTime} ms`); + + // + // Connect to PostgreSQL + // https://node-postgres.com/ + console.log( + `Establishing connection to PostgreSQL database using SM secret '${res.result.name}' (last updated: '${res.result.updated_at}') ...` + ); + const pgCaCert = Buffer.from(res.result.credentials.connection.postgres.certificate.certificate_base64, "base64"); + const pgConnectionString = res.result.credentials.connection.postgres.composed[0]; + _pgPool = await connectDb(pgConnectionString, pgCaCert); + + console.log(`${fn} < done - duration ${Date.now() - startTime} ms`); + return _pgPool; +} + +export function addUser(client, firstName, lastName) { + const fn = "addUser "; + const startTime = Date.now(); + console.log(`${fn} > firstName: '${firstName}', lastName: '${lastName}'`); + return new Promise(function (resolve, reject) { + const queryText = "INSERT INTO users(firstname,lastname) VALUES($1, $2)"; + client.query(queryText, [firstName, lastName], function (error, result) { + if (error) { + console.log(`${fn} < failed - error: ${error}; duration ${Date.now() - startTime} ms`); + return reject(error); + } + console.log(`${fn} < succeeded - duration ${Date.now() - startTime} ms`); + return resolve(result); + }); + }); +} + +export function listUsers(client) { + const fn = "listUsers "; + const startTime = Date.now(); + console.log(`${fn} >`); + return new Promise(function (resolve, reject) { + const queryText = "SELECT * FROM users"; + client.query(queryText, undefined, function (error, result) { + if (error) { + console.log(`${fn} < failed - error: ${error}; duration ${Date.now() - startTime} ms`); + return reject(error); + } + console.log(`${fn} < succeeded - duration ${Date.now() - startTime} ms`); + return resolve(result); + }); + }); +} +export function deleteUsers(client) { + const fn = "deleteUsers "; + const startTime = Date.now(); + console.log(`${fn} >`); + return new Promise(function (resolve, reject) { + const queryText = "DELETE FROM users"; + client.query(queryText, undefined, function (error, result) { + if (error) { + console.log(`${fn} < failed - error: ${error}; duration ${Date.now() - startTime} ms`); + return reject(error); + } + console.log(`${fn} < succeeded - duration ${Date.now() - startTime} ms`); + return resolve(result); + }); + }); +} diff --git a/cos-to-sql/utils/utils.mjs b/cos-to-sql/utils/utils.mjs new file mode 100644 index 00000000..09d760fc --- /dev/null +++ b/cos-to-sql/utils/utils.mjs @@ -0,0 +1,28 @@ +// Libraries to convert CSV content into a object structure +import csv from "csv-parser"; +import { Readable } from "stream"; + +// helper function to convert a CSV data structure into an array of objects +export function convertCsvToDataStruct(csvContent) { + return new Promise((resolve) => { + // the result to return + const results = []; + + // create a new readable stream + var readableStream = new Readable(); + + // the CSV parser consumes the stream + readableStream + .pipe(csv()) + .on("data", (data) => results.push(data)) + .on("end", () => { + console.log(`converted CSV data: ${JSON.stringify(results)}`); + + resolve(results); + }); + + // push the CSV file content to the stream + readableStream.push(csvContent); + readableStream.push(null); // indicates end-of-file + }); +} From c4f024b064c3c497d597efa27fa48c0c5bf81aa6 Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Sat, 5 Apr 2025 01:05:00 +0200 Subject: [PATCH 05/17] enabled graceful shutdowns --- cos-to-sql/README.md | 5 ++-- cos-to-sql/app.mjs | 21 ++++++++------- cos-to-sql/utils/db.mjs | 58 +++++++++++++++++++---------------------- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/cos-to-sql/README.md b/cos-to-sql/README.md index 5a74f5ea..2e64153b 100644 --- a/cos-to-sql/README.md +++ b/cos-to-sql/README.md @@ -78,7 +78,8 @@ ibmcloud resource service-instance-create $SM_INSTANCE_NAME secrets-manager 7713 export SM_INSTANCE_ID=$(ibmcloud resource service-instance $SM_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .id') export SM_INSTANCE_GUID=$(ibmcloud resource service-instance $SM_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .guid') -export SECRETS_MANAGER_URL=https://$SM_INSTANCE_GUID.${REGION}.secrets-manager.appdomain.cloud +export SECRETS_MANAGER_URL=https://${SM_INSTANCE_GUID}.${REGION}.secrets-manager.appdomain.cloud +export SECRETS_MANAGER_URL_PRIVATE=https://${SM_INSTANCE_GUID}.private.${REGION}.secrets-manager.appdomain.cloud ``` * Create a S2S policy "Key Manager" between SM and the DB @@ -116,7 +117,7 @@ ibmcloud code-engine app create \ --env COS_REGION=${REGION} \ --env COS_TRUSTED_PROFILE_NAME=${TRUSTED_PROFILE_FOR_COS_NAME} \ --env SM_TRUSTED_PROFILE_NAME=${TRUSTED_PROFILE_FOR_SM_NAME} \ - --env SM_SERVICE_URL=${SECRETS_MANAGER_URL} \ + --env SM_SERVICE_URL=${SECRETS_MANAGER_URL_PRIVATE} \ --env SM_PG_SECRET_ID=${SM_SECRET_FOR_PG_ID} ``` diff --git a/cos-to-sql/app.mjs b/cos-to-sql/app.mjs index c98740cf..fde2f45a 100644 --- a/cos-to-sql/app.mjs +++ b/cos-to-sql/app.mjs @@ -71,6 +71,17 @@ app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))) const router = express.Router(); app.use("/", router); + +// +// Default http endpoint, which prints the list of all users in the database +router.get("/", async (req, res) => { + console.log(`handling / for '${req.url}'`); + const pgClient = await getPgClient(secretsManager, smPgSecretId); + const allUsers = await listUsers(pgClient); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ users: allUsers.rows })); +}); + // // Readiness endpoint router.get("/readiness", (req, res) => { @@ -177,15 +188,6 @@ router.get("/clear", async (req, res) => { return; }); -// -// Default http endpoint, which prints the list of all users in the database -router.get("/", async (req, res) => { - console.log(`handling / for '${req.url}'`); - const pgClient = await getPgClient(secretsManager, smPgSecretId); - const allUsers = await listUsers(pgClient); - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ users: allUsers.rows })); -}); // start server const port = process.env.PORT || 8080; @@ -195,6 +197,7 @@ const server = app.listen(port, () => { process.on("SIGTERM", async () => { console.info("SIGTERM signal received."); + await closeDBConnection(); server.close(() => { diff --git a/cos-to-sql/utils/db.mjs b/cos-to-sql/utils/db.mjs index 1b4398b7..03825853 100644 --- a/cos-to-sql/utils/db.mjs +++ b/cos-to-sql/utils/db.mjs @@ -17,42 +17,26 @@ function connectDb(connectionString, caCert) { }; // set up a new client using our config details - let client = new Pool(postgreConfig); + let pool = new Pool({ ...postgreConfig, max: 20, idleTimeoutMillis: 5000, connectionTimeoutMillis: 2000 }); - client.connect((err) => { - if (err) { - console.error(`Failed to connect to postgreSQL host '${postgreConfig.host}'`, err); - return reject(err); - } - - client.query( - "CREATE TABLE IF NOT EXISTS users (firstname varchar(256) NOT NULL, lastname varchar(256) NOT NULL)", - (err, result) => { - if (err) { - console.log(`Failed to create PostgreSQL table 'users'`, err); - return reject(err); - } - console.log( - `Established PostgreSQL client connection to '${postgreConfig.host}' - user table init: ${JSON.stringify( - result - )}` - ); - return resolve(client); + pool.query( + "CREATE TABLE IF NOT EXISTS users (firstname varchar(256) NOT NULL, lastname varchar(256) NOT NULL)", + (err, result) => { + if (err) { + console.log(`Failed to create PostgreSQL table 'users'`, err); + return reject(err); } - ); - }); + console.log( + `Established PostgreSQL client connection to '${postgreConfig.host}' - user table init: ${JSON.stringify( + result + )}` + ); + return resolve(pool); + } + ); }); } -export async function closeDBConnection() { - if (_pgPool) { - console.log("Draining PG pool."); - await _pgPool.end(); - console.log("PG pool has drained."); - } - Promise.resolve(); -} - export async function getPgClient(secretsManager, secretId) { const fn = "getPgClient "; const startTime = Date.now(); @@ -81,6 +65,10 @@ export async function getPgClient(secretsManager, secretId) { const pgConnectionString = res.result.credentials.connection.postgres.composed[0]; _pgPool = await connectDb(pgConnectionString, pgCaCert); + _pgPool.on("error", (err) => { + console.log("Pool received an error event", err); + }); + console.log(`${fn} < done - duration ${Date.now() - startTime} ms`); return _pgPool; } @@ -134,3 +122,11 @@ export function deleteUsers(client) { }); }); } + +export async function closeDBConnection() { + if (_pgPool) { + console.log("Draining PG pool..."); + await _pgPool.end(); + console.log("PG pool has drained."); + } +} From 3244ee9fb31fd66d307e4e71530f11db3fd9f651 Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Sun, 6 Apr 2025 23:55:13 +0200 Subject: [PATCH 06/17] Added the draw.io file and adjusted the readme --- cos-to-sql/README.md | 34 +- cos-to-sql/build | 19 + .../docs/ce-trusted-profiles-samples.drawio | 716 ++++++++++++++++++ .../trusted-profiles-part2-arch-overview.png | Bin 0 -> 160749 bytes cos-to-sql/samples/users.csv | 7 +- 5 files changed, 768 insertions(+), 8 deletions(-) create mode 100755 cos-to-sql/build create mode 100644 cos-to-sql/docs/ce-trusted-profiles-samples.drawio create mode 100644 cos-to-sql/docs/trusted-profiles-part2-arch-overview.png diff --git a/cos-to-sql/README.md b/cos-to-sql/README.md index 2e64153b..d80ac33a 100644 --- a/cos-to-sql/README.md +++ b/cos-to-sql/README.md @@ -2,6 +2,8 @@ This sample demonstrates how to read CSV files hosted on a IBM Cloud Object Storage and save their contents line by line into relational PostgreSQL database. +![Architecture overview](./docs/trusted-profiles-part2-arch-overview.png) + ## Prerequisites Make sure the following [IBM Cloud CLI](https://cloud.ibm.com/docs/cli/reference/ibmcloud?topic=cloud-cli-getting-started) and the following list of plugins are installed @@ -23,7 +25,7 @@ ibmcloud login -r ${REGION} -g $RESOURCE_GROUP * Create the Code Engine project ``` export CE_INSTANCE_NAME=cos-to-sql--ce -ibmcloud code-engine project create -n ${CE_INSTANCE_NAME} +ibmcloud code-engine project create --name ${CE_INSTANCE_NAME} export CE_INSTANCE_GUID=$(ibmcloud ce project current -o json | jq -r .guid) export CE_INSTANCE_ID=$(ibmcloud resource service-instance ${CE_INSTANCE_NAME} --output json | jq -r '.[0] | .id') @@ -93,7 +95,10 @@ ibmcloud iam authorization-policy-create secrets-manager databases-for-postgresq * Create the service credential to access the PostgreSQL instance ``` SM_SECRET_FOR_PG_NAME=pg-access-credentials -ibmcloud secrets-manager secret-create --secret-type="service_credentials" --secret-name="$SM_SECRET_FOR_PG_NAME" --secret-source-service="{\"instance\": {\"crn\": \"$DB_INSTANCE_ID\"},\"parameters\": {},\"role\": {\"crn\": \"crn:v1:bluemix:public:iam::::serviceRole:Writer\"}}" +ibmcloud secrets-manager secret-create \ + --secret-type="service_credentials" \ + --secret-name="$SM_SECRET_FOR_PG_NAME" \ + --secret-source-service="{\"instance\": {\"crn\": \"$DB_INSTANCE_ID\"},\"parameters\": {},\"role\": {\"crn\": \"crn:v1:bluemix:public:iam::::serviceRole:Writer\"}}" export SM_SECRET_FOR_PG_ID=$(ibmcloud sm secret-by-name --name $SM_SECRET_FOR_PG_NAME --secret-type service_credentials --secret-group-name default --output JSON|jq -r '.id') ``` @@ -126,16 +131,32 @@ ibmcloud code-engine app create \ * Create a trusted profile that grants a Code Engine app access to your COS bucket ``` ibmcloud iam trusted-profile-create ${TRUSTED_PROFILE_FOR_COS_NAME} -ibmcloud iam trusted-profile-link-create ${TRUSTED_PROFILE_FOR_COS_NAME} --name ce-app-${CE_APP_NAME} --cr-type CE --link-crn ${CE_INSTANCE_ID} --link-component-type application --link-component-name ${CE_APP_NAME} -ibmcloud iam trusted-profile-policy-create ${TRUSTED_PROFILE_FOR_COS_NAME} --roles "Content Reader" --service-name cloud-object-storage --service-instance ${COS_INSTANCE_ID} --resource-type bucket --resource ${COS_BUCKET_NAME} +ibmcloud iam trusted-profile-link-create ${TRUSTED_PROFILE_FOR_COS_NAME} \ + --name ce-app-${CE_APP_NAME} \ + --cr-type CE --link-crn ${CE_INSTANCE_ID} \ + --link-component-type application \ + --link-component-name ${CE_APP_NAME} +ibmcloud iam trusted-profile-policy-create ${TRUSTED_PROFILE_FOR_COS_NAME} \ + --roles "Content Reader" \ + --service-name cloud-object-storage \ + --service-instance ${COS_INSTANCE_ID} \ + --resource-type bucket \ + --resource ${COS_BUCKET_NAME} ``` * Create the trusted profile to access Secrets Manager ``` ibmcloud iam trusted-profile-create ${TRUSTED_PROFILE_FOR_SM_NAME} -ibmcloud iam trusted-profile-link-create ${TRUSTED_PROFILE_FOR_SM_NAME} --name ce-app-${CE_APP_NAME} --cr-type CE --link-crn ${CE_INSTANCE_ID} --link-component-type application --link-component-name ${CE_APP_NAME} -ibmcloud iam trusted-profile-policy-create ${TRUSTED_PROFILE_FOR_SM_NAME} --roles "SecretsReader" --service-name secrets-manager --service-instance ${SM_INSTANCE_ID} +ibmcloud iam trusted-profile-link-create ${TRUSTED_PROFILE_FOR_SM_NAME} \ + --name ce-app-${CE_APP_NAME} \ + --cr-type CE --link-crn ${CE_INSTANCE_ID} \ + --link-component-type application \ + --link-component-name ${CE_APP_NAME} +ibmcloud iam trusted-profile-policy-create ${TRUSTED_PROFILE_FOR_SM_NAME} \ + --roles "SecretsReader" \ + --service-name secrets-manager \ + --service-instance ${SM_INSTANCE_ID} ``` ## Setting up eventing @@ -153,6 +174,7 @@ ibmcloud iam authorization-policy-create codeengine cloud-object-storage \ ibmcloud ce sub cos create \ --name "coswatch-${CE_APP_NAME}" \ --bucket ${COS_BUCKET_NAME} \ + --event-type "write" \ --destination ${CE_APP_NAME} \ --destination-type app \ --path /cos-to-sql diff --git a/cos-to-sql/build b/cos-to-sql/build new file mode 100755 index 00000000..8e682b96 --- /dev/null +++ b/cos-to-sql/build @@ -0,0 +1,19 @@ +#!/bin/bash + +# Env Vars: +# REGISTRY: name of the image registry/namespace to store the images +# NOCACHE: set this to "--no-cache" to turn off the Docker build cache +# +# NOTE: to run this you MUST set the REGISTRY environment variable to +# your own image registry/namespace otherwise the `docker push` commands +# will fail due to an auth failure. Which means, you also need to be logged +# into that registry before you run it. + +set -ex +export REGISTRY=${REGISTRY:-icr.io/codeengine} + +# Build the image +docker build ${NOCACHE} -t ${REGISTRY}/cos-to-sql . --platform linux/amd64 + +# And push it +docker push ${REGISTRY}/cos-to-sql diff --git a/cos-to-sql/docs/ce-trusted-profiles-samples.drawio b/cos-to-sql/docs/ce-trusted-profiles-samples.drawio new file mode 100644 index 00000000..26681be1 --- /dev/null +++ b/cos-to-sql/docs/ce-trusted-profiles-samples.drawio @@ -0,0 +1,716 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cos-to-sql/docs/trusted-profiles-part2-arch-overview.png b/cos-to-sql/docs/trusted-profiles-part2-arch-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..754d79a00d4121a6b26f3657fce2ed1ba353cf60 GIT binary patch literal 160749 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 Date: Mon, 7 Apr 2025 14:09:03 +0200 Subject: [PATCH 07/17] Update cos-to-sql/Dockerfile Co-authored-by: Sascha Schwarze --- cos-to-sql/Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cos-to-sql/Dockerfile b/cos-to-sql/Dockerfile index c5284fbd..81af55db 100644 --- a/cos-to-sql/Dockerfile +++ b/cos-to-sql/Dockerfile @@ -10,9 +10,7 @@ RUN npm ci --omit=dev FROM gcr.io/distroless/nodejs22 COPY --chown=1001:0 --from=builder /app/node_modules /app/node_modules -COPY --chown=1001:0 app.mjs /app -COPY --chown=1001:0 utils/ /app/utils -COPY --chown=1001:0 public/ /app/public +COPY --chown=1001:0 app.mjs utils public /app/ USER 1001:0 WORKDIR /app From 50129afbac68d3ea195af2dcacb426b4cd6a58d9 Mon Sep 17 00:00:00 2001 From: Enrico Regge <36001299+reggeenr@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:09:12 +0200 Subject: [PATCH 08/17] Update cos-to-sql/README.md Co-authored-by: Sascha Schwarze --- cos-to-sql/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cos-to-sql/README.md b/cos-to-sql/README.md index d80ac33a..62d3cc1a 100644 --- a/cos-to-sql/README.md +++ b/cos-to-sql/README.md @@ -6,7 +6,7 @@ This sample demonstrates how to read CSV files hosted on a IBM Cloud Object Stor ## Prerequisites -Make sure the following [IBM Cloud CLI](https://cloud.ibm.com/docs/cli/reference/ibmcloud?topic=cloud-cli-getting-started) and the following list of plugins are installed +Make sure the [IBM Cloud CLI](https://cloud.ibm.com/docs/cli/reference/ibmcloud?topic=cloud-cli-getting-started) and the following list of plugins are installed - `ibmcloud plugin install code-engine` - `ibmcloud plugin install cloud-object-storage` - `ibmcloud plugin install secrets-manager` From 7e1239e864741118de99f46597f5518ce30c7695 Mon Sep 17 00:00:00 2001 From: Enrico Regge <36001299+reggeenr@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:09:48 +0200 Subject: [PATCH 09/17] Update cos-to-sql/README.md Co-authored-by: Sascha Schwarze --- cos-to-sql/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cos-to-sql/README.md b/cos-to-sql/README.md index 62d3cc1a..f2b02ca7 100644 --- a/cos-to-sql/README.md +++ b/cos-to-sql/README.md @@ -19,7 +19,7 @@ Install `jq`. On MacOS, you can use following [brew formulae](https://formulae.b ``` export REGION=ca-tor export RESOURCE_GROUP=Default -ibmcloud login -r ${REGION} -g $RESOURCE_GROUP +ibmcloud login -r ${REGION} -g ${RESOURCE_GROUP} ``` * Create the Code Engine project From 62ccd492c0babe8d83b975158eb938a0ce5613a2 Mon Sep 17 00:00:00 2001 From: Enrico Regge <36001299+reggeenr@users.noreply.github.com> Date: Mon, 7 Apr 2025 17:51:25 +0200 Subject: [PATCH 10/17] Update cos-to-sql/README.md Co-authored-by: Sascha Schwarze --- cos-to-sql/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cos-to-sql/README.md b/cos-to-sql/README.md index f2b02ca7..db117fae 100644 --- a/cos-to-sql/README.md +++ b/cos-to-sql/README.md @@ -169,7 +169,7 @@ ibmcloud iam authorization-policy-create codeengine cloud-object-storage \ --target-service-instance-id ${COS_INSTANCE_ID} ``` -* Create the subscription for all COS events: +* Create the subscription for COS events of type "write": ``` ibmcloud ce sub cos create \ --name "coswatch-${CE_APP_NAME}" \ From 0e9e67718bfafeb9d14ae5684f6273d99d0f4bb7 Mon Sep 17 00:00:00 2001 From: Enrico Regge <36001299+reggeenr@users.noreply.github.com> Date: Mon, 7 Apr 2025 17:51:53 +0200 Subject: [PATCH 11/17] Update cos-to-sql/.dockerignore Co-authored-by: Sascha Schwarze --- cos-to-sql/.dockerignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cos-to-sql/.dockerignore b/cos-to-sql/.dockerignore index 6bb19bf7..03d6804f 100644 --- a/cos-to-sql/.dockerignore +++ b/cos-to-sql/.dockerignore @@ -2,4 +2,5 @@ .gitignore build Dockerfile -node_modules \ No newline at end of file +node_modules +samples \ No newline at end of file From 3b2fa9fd57b7c49c367205ca1525a7e8247ab152 Mon Sep 17 00:00:00 2001 From: Enrico Regge <36001299+reggeenr@users.noreply.github.com> Date: Mon, 7 Apr 2025 17:52:15 +0200 Subject: [PATCH 12/17] Update cos-to-sql/.ceignore Co-authored-by: Sascha Schwarze --- cos-to-sql/.ceignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cos-to-sql/.ceignore b/cos-to-sql/.ceignore index b512c09d..22ab6b17 100644 --- a/cos-to-sql/.ceignore +++ b/cos-to-sql/.ceignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +samples \ No newline at end of file From 967a25a3155f580570242de525d24001fbe7c281 Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Tue, 8 Apr 2025 23:09:41 +0200 Subject: [PATCH 13/17] Addressed review comments --- cos-to-sql/README.md | 52 ++++++++++++++++++++++++-------------------- cos-to-sql/app.mjs | 41 ++++++++++++++-------------------- 2 files changed, 44 insertions(+), 49 deletions(-) diff --git a/cos-to-sql/README.md b/cos-to-sql/README.md index d80ac33a..df824c15 100644 --- a/cos-to-sql/README.md +++ b/cos-to-sql/README.md @@ -1,6 +1,6 @@ # IBM Cloud Code Engine - Integrate Cloud Object Storage and PostgreSQL through a app and an event subscription -This sample demonstrates how to read CSV files hosted on a IBM Cloud Object Storage and save their contents line by line into relational PostgreSQL database. +This sample demonstrates how to read CSV files hosted on a IBM Cloud Object Storage and save their contents line by line into relational PostgreSQL database, leveraging IAM trusted profiles. ![Architecture overview](./docs/trusted-profiles-part2-arch-overview.png) @@ -17,26 +17,26 @@ Install `jq`. On MacOS, you can use following [brew formulae](https://formulae.b * Login to IBM Cloud via the CLI and target the `ca-tor` region: ``` -export REGION=ca-tor -export RESOURCE_GROUP=Default +REGION=ca-tor +RESOURCE_GROUP=Default ibmcloud login -r ${REGION} -g $RESOURCE_GROUP ``` * Create the Code Engine project ``` -export CE_INSTANCE_NAME=cos-to-sql--ce +CE_INSTANCE_NAME=cos-to-sql--ce ibmcloud code-engine project create --name ${CE_INSTANCE_NAME} -export CE_INSTANCE_GUID=$(ibmcloud ce project current -o json | jq -r .guid) -export CE_INSTANCE_ID=$(ibmcloud resource service-instance ${CE_INSTANCE_NAME} --output json | jq -r '.[0] | .id') +CE_INSTANCE_GUID=$(ibmcloud ce project current -o json | jq -r .guid) +CE_INSTANCE_ID=$(ibmcloud resource service-instance ${CE_INSTANCE_NAME} --output json | jq -r '.[0] | .id') ``` * Create the COS instance ``` -export COS_INSTANCE_NAME=cos-to-sql--cos +COS_INSTANCE_NAME=cos-to-sql--cos ibmcloud resource service-instance-create ${COS_INSTANCE_NAME} cloud-object-storage standard global -export COS_INSTANCE_ID=$(ibmcloud resource service-instance ${COS_INSTANCE_NAME} --output json | jq -r '.[0] | .id') +COS_INSTANCE_ID=$(ibmcloud resource service-instance ${COS_INSTANCE_NAME} --output json | jq -r '.[0] | .id') ``` * Create a COS bucket @@ -45,7 +45,7 @@ ibmcloud cos config crn --crn ${COS_INSTANCE_ID} --force ibmcloud cos config auth --method IAM ibmcloud cos config region --region ${REGION} ibmcloud cos config endpoint-url --url s3.${REGION}.cloud-object-storage.appdomain.cloud -export COS_BUCKET_NAME=${CE_INSTANCE_GUID}-csv-to-sql +COS_BUCKET_NAME=${CE_INSTANCE_GUID}-csv-to-sql ibmcloud cos bucket-create \ --class smart \ --bucket $COS_BUCKET_NAME @@ -53,7 +53,7 @@ ibmcloud cos bucket-create \ * Create the PostgreSQL instance ``` -export DB_INSTANCE_NAME=cos-to-sql--pg +DB_INSTANCE_NAME=cos-to-sql--pg ibmcloud resource service-instance-create $DB_INSTANCE_NAME databases-for-postgresql standard ${REGION} --service-endpoints private -p \ '{ "disk_encryption_instance_crn": "none", @@ -67,21 +67,21 @@ ibmcloud resource service-instance-create $DB_INSTANCE_NAME databases-for-postgr "version": "16" }' -export DB_INSTANCE_ID=$(ibmcloud resource service-instance $DB_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .id') +DB_INSTANCE_ID=$(ibmcloud resource service-instance $DB_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .id') ``` * Create the Secrets Manager instance ``` -export SM_INSTANCE_NAME=cos-to-sql--sm +SM_INSTANCE_NAME=cos-to-sql--sm ibmcloud resource service-instance-create $SM_INSTANCE_NAME secrets-manager 7713c3a8-3be8-4a9a-81bb-ee822fcaac3d ${REGION} -p \ '{ "allowed_network": "public-and-private" }' -export SM_INSTANCE_ID=$(ibmcloud resource service-instance $SM_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .id') -export SM_INSTANCE_GUID=$(ibmcloud resource service-instance $SM_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .guid') -export SECRETS_MANAGER_URL=https://${SM_INSTANCE_GUID}.${REGION}.secrets-manager.appdomain.cloud -export SECRETS_MANAGER_URL_PRIVATE=https://${SM_INSTANCE_GUID}.private.${REGION}.secrets-manager.appdomain.cloud +SM_INSTANCE_ID=$(ibmcloud resource service-instance $SM_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .id') +SM_INSTANCE_GUID=$(ibmcloud resource service-instance $SM_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .guid') +SECRETS_MANAGER_URL=https://${SM_INSTANCE_GUID}.${REGION}.secrets-manager.appdomain.cloud +SECRETS_MANAGER_URL_PRIVATE=https://${SM_INSTANCE_GUID}.private.${REGION}.secrets-manager.appdomain.cloud ``` * Create a S2S policy "Key Manager" between SM and the DB @@ -100,21 +100,21 @@ ibmcloud secrets-manager secret-create \ --secret-name="$SM_SECRET_FOR_PG_NAME" \ --secret-source-service="{\"instance\": {\"crn\": \"$DB_INSTANCE_ID\"},\"parameters\": {},\"role\": {\"crn\": \"crn:v1:bluemix:public:iam::::serviceRole:Writer\"}}" -export SM_SECRET_FOR_PG_ID=$(ibmcloud sm secret-by-name --name $SM_SECRET_FOR_PG_NAME --secret-type service_credentials --secret-group-name default --output JSON|jq -r '.id') +SM_SECRET_FOR_PG_ID=$(ibmcloud sm secret-by-name --name $SM_SECRET_FOR_PG_NAME --secret-type service_credentials --secret-group-name default --output JSON|jq -r '.id') ``` * Create the Code Engine app: ``` -export CE_APP_NAME=csv-to-sql -export TRUSTED_PROFILE_FOR_COS_NAME=cos-to-sql--ce-to-cos-access -export TRUSTED_PROFILE_FOR_SM_NAME=cos-to-sql--ce-to-sm-access +CE_APP_NAME=csv-to-sql +TRUSTED_PROFILE_FOR_COS_NAME=cos-to-sql--ce-to-cos-access +TRUSTED_PROFILE_FOR_SM_NAME=cos-to-sql--ce-to-sm-access ibmcloud code-engine app create \ --name ${CE_APP_NAME} \ - --source ./ \ - --cpu 0.25 \ - --memory 0.5G \ + --build-source https://github.com/IBM/CodeEngine \ + --build-commit + --build-context-dir cos-to-sql/ \ --trusted-profiles-enabled="true" \ --probe-ready type=http \ --probe-ready path=/readiness \ @@ -184,10 +184,14 @@ ibmcloud ce sub cos create \ * Upload a CSV file to COS, to initate an event that leads to a job execution: ``` +curl -s https://raw.githubusercontent.com/IBM/CodeEngine/main/cos-to-sql/samples/users.csv > CodeEngine-sample-users.csv + +cat CodeEngine-sample-users.csv + ibmcloud cos object-put \ --bucket ${COS_BUCKET_NAME} \ --key users.csv \ - --body ./samples/users.csv \ + --body ./CodeEngine-sample-users.csv \ --content-type text/csv ``` diff --git a/cos-to-sql/app.mjs b/cos-to-sql/app.mjs index fde2f45a..877e6e8f 100644 --- a/cos-to-sql/app.mjs +++ b/cos-to-sql/app.mjs @@ -1,4 +1,4 @@ -import { existsSync } from "fs"; +import { stat } from "fs/promises"; import express from "express"; import favicon from "serve-favicon"; import path from "path"; @@ -37,41 +37,33 @@ requiredEnvVars.forEach((envVarName) => { // Initialize COS const cosRegion = process.env.COS_REGION; const cosTrustedProfileName = process.env.COS_TRUSTED_PROFILE_NAME; -let cosAuthenticator; -if (cosTrustedProfileName) { - // create an authenticator to access the COS instance based on a trusted profile - cosAuthenticator = new ContainerAuthenticator({ - iamProfileName: cosTrustedProfileName, - }); -} +// Create an authenticator to access the COS instance based on a trusted profile +const cosAuthenticator = new ContainerAuthenticator({ + iamProfileName: cosTrustedProfileName, +}); // // Initialize Secrets Manager SDK const smTrustedProfileName = process.env.SM_TRUSTED_PROFILE_NAME; const smServiceURL = process.env.SM_SERVICE_URL; const smPgSecretId = process.env.SM_PG_SECRET_ID; -let secretsManager; -if (smTrustedProfileName && smServiceURL) { - // create an authenticator to access the SecretsManager instance based on a trusted profile - const smAuthenticator = new ContainerAuthenticator({ +// Create an instance of the SDK by providing an authentication mechanism and your Secrets Manager instance URL +const secretsManager = new SecretsManager({ + // Create an authenticator to access the SecretsManager instance based on a trusted profile + authenticator: new ContainerAuthenticator({ iamProfileName: smTrustedProfileName, - }); - // Create an instance of the SDK by providing an authentication mechanism and your Secrets Manager instance URL - secretsManager = new SecretsManager({ - authenticator: smAuthenticator, - serviceUrl: smServiceURL, - }); -} + }), + serviceUrl: smServiceURL, +}); const app = express(); app.use(express.json()); -app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))) +app.use(favicon(path.join(__dirname, "public", "favicon.ico"))); // use router to bundle all routes to / const router = express.Router(); app.use("/", router); - // // Default http endpoint, which prints the list of all users in the database router.get("/", async (req, res) => { @@ -84,9 +76,9 @@ router.get("/", async (req, res) => { // // Readiness endpoint -router.get("/readiness", (req, res) => { +router.get("/readiness", async (req, res) => { console.log(`handling /readiness`); - if (!existsSync("/var/run/secrets/codeengine.cloud.ibm.com/compute-resource-token/token")) { + if (!await stat("/var/run/secrets/codeengine.cloud.ibm.com/compute-resource-token/token")) { console.error("Mounting the trusted profile compute resource token is not enabled"); res.writeHead(500, { "Content-Type": "application/json" }); res.end('{"error": "Mounting the trusted profile compute resource token is not enabled"}'); @@ -114,7 +106,7 @@ router.post("/cos-to-sql", async (req, res) => { res.end('{"error": "request does not contain any event data"}'); return; } - + // // make sure that the event relates to a COS write operation if (event.notification.event_type !== "Object:Write") { @@ -188,7 +180,6 @@ router.get("/clear", async (req, res) => { return; }); - // start server const port = process.env.PORT || 8080; const server = app.listen(port, () => { From 40c4b13ba485f3fb7ddb76320c7e91db152fbbda Mon Sep 17 00:00:00 2001 From: Enrico Regge <36001299+reggeenr@users.noreply.github.com> Date: Wed, 16 Apr 2025 09:13:25 +0200 Subject: [PATCH 14/17] Update cos-to-sql/README.md Co-authored-by: Sascha Schwarze --- cos-to-sql/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/cos-to-sql/README.md b/cos-to-sql/README.md index 2028db95..eff9e70a 100644 --- a/cos-to-sql/README.md +++ b/cos-to-sql/README.md @@ -113,7 +113,6 @@ TRUSTED_PROFILE_FOR_SM_NAME=cos-to-sql--ce-to-sm-access ibmcloud code-engine app create \ --name ${CE_APP_NAME} \ --build-source https://github.com/IBM/CodeEngine \ - --build-commit --build-context-dir cos-to-sql/ \ --trusted-profiles-enabled="true" \ --probe-ready type=http \ From 90577e4a6c35e6ad4889d6ddaf8f3dfa60f3792f Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Thu, 17 Apr 2025 11:41:37 +0200 Subject: [PATCH 15/17] Addressed review comments --- .gitignore | 3 +- cos-to-sql/.ceignore | 4 +- cos-to-sql/.dockerignore | 3 +- cos-to-sql/Dockerfile | 2 +- cos-to-sql/README.md | 291 +++++++++++++++++++-------------------- cos-to-sql/app.mjs | 39 +++--- cos-to-sql/utils/db.mjs | 6 +- 7 files changed, 171 insertions(+), 177 deletions(-) diff --git a/.gitignore b/.gitignore index 345815a3..d8f054eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.out -node_modules \ No newline at end of file +node_modules +.DS_Store \ No newline at end of file diff --git a/cos-to-sql/.ceignore b/cos-to-sql/.ceignore index 22ab6b17..6fb7772d 100644 --- a/cos-to-sql/.ceignore +++ b/cos-to-sql/.ceignore @@ -1,2 +1,4 @@ node_modules -samples \ No newline at end of file +samples +docs +.DS_Store \ No newline at end of file diff --git a/cos-to-sql/.dockerignore b/cos-to-sql/.dockerignore index 03d6804f..4ff99abb 100644 --- a/cos-to-sql/.dockerignore +++ b/cos-to-sql/.dockerignore @@ -3,4 +3,5 @@ build Dockerfile node_modules -samples \ No newline at end of file +samples +build \ No newline at end of file diff --git a/cos-to-sql/Dockerfile b/cos-to-sql/Dockerfile index 81af55db..6fa3a483 100644 --- a/cos-to-sql/Dockerfile +++ b/cos-to-sql/Dockerfile @@ -10,7 +10,7 @@ RUN npm ci --omit=dev FROM gcr.io/distroless/nodejs22 COPY --chown=1001:0 --from=builder /app/node_modules /app/node_modules -COPY --chown=1001:0 app.mjs utils public /app/ +COPY --chown=1001:0 . /app/ USER 1001:0 WORKDIR /app diff --git a/cos-to-sql/README.md b/cos-to-sql/README.md index 2028db95..28461f9d 100644 --- a/cos-to-sql/README.md +++ b/cos-to-sql/README.md @@ -16,188 +16,185 @@ Install `jq`. On MacOS, you can use following [brew formulae](https://formulae.b ## Setting up all IBM Cloud Service instances * Login to IBM Cloud via the CLI and target the `ca-tor` region: -``` -REGION=ca-tor -RESOURCE_GROUP=Default -ibmcloud login -r ${REGION} -g ${RESOURCE_GROUP} -``` + ``` + REGION=ca-tor + RESOURCE_GROUP=Default + ibmcloud login -r ${REGION} -g ${RESOURCE_GROUP} + ``` * Create the Code Engine project -``` -CE_INSTANCE_NAME=cos-to-sql--ce -ibmcloud code-engine project create --name ${CE_INSTANCE_NAME} + ``` + CE_INSTANCE_NAME=cos-to-sql--ce + ibmcloud code-engine project create --name ${CE_INSTANCE_NAME} -CE_INSTANCE_GUID=$(ibmcloud ce project current -o json | jq -r .guid) -CE_INSTANCE_ID=$(ibmcloud resource service-instance ${CE_INSTANCE_NAME} --output json | jq -r '.[0] | .id') -``` + CE_INSTANCE_GUID=$(ibmcloud ce project current -o json | jq -r .guid) + CE_INSTANCE_ID=$(ibmcloud resource service-instance ${CE_INSTANCE_NAME} --output json | jq -r '.[0] | .id') + ``` * Create the COS instance -``` -COS_INSTANCE_NAME=cos-to-sql--cos -ibmcloud resource service-instance-create ${COS_INSTANCE_NAME} cloud-object-storage standard global + ``` + COS_INSTANCE_NAME=cos-to-sql--cos + ibmcloud resource service-instance-create ${COS_INSTANCE_NAME} cloud-object-storage standard global -COS_INSTANCE_ID=$(ibmcloud resource service-instance ${COS_INSTANCE_NAME} --output json | jq -r '.[0] | .id') -``` + COS_INSTANCE_ID=$(ibmcloud resource service-instance ${COS_INSTANCE_NAME} --output json | jq -r '.[0] | .id') + ``` * Create a COS bucket -``` -ibmcloud cos config crn --crn ${COS_INSTANCE_ID} --force -ibmcloud cos config auth --method IAM -ibmcloud cos config region --region ${REGION} -ibmcloud cos config endpoint-url --url s3.${REGION}.cloud-object-storage.appdomain.cloud -COS_BUCKET_NAME=${CE_INSTANCE_GUID}-csv-to-sql -ibmcloud cos bucket-create \ - --class smart \ - --bucket $COS_BUCKET_NAME -``` + ``` + ibmcloud cos config crn --crn ${COS_INSTANCE_ID} --force + ibmcloud cos config auth --method IAM + ibmcloud cos config region --region ${REGION} + ibmcloud cos config endpoint-url --url s3.${REGION}.cloud-object-storage.appdomain.cloud + COS_BUCKET_NAME=${CE_INSTANCE_GUID}-csv-to-sql + ibmcloud cos bucket-create \ + --class smart \ + --bucket $COS_BUCKET_NAME + ``` * Create the PostgreSQL instance -``` -DB_INSTANCE_NAME=cos-to-sql--pg -ibmcloud resource service-instance-create $DB_INSTANCE_NAME databases-for-postgresql standard ${REGION} --service-endpoints private -p \ - '{ - "disk_encryption_instance_crn": "none", - "disk_encryption_key_crn": "none", - "members_cpu_allocation_count": "0 cores", - "members_disk_allocation_mb": "10240MB", - "members_host_flavor": "multitenant", - "members_members_allocation_count": 2, - "members_memory_allocation_mb": "8192MB", - "service-endpoints": "private", - "version": "16" -}' - -DB_INSTANCE_ID=$(ibmcloud resource service-instance $DB_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .id') -``` - -* Create the Secrets Manager instance -``` -SM_INSTANCE_NAME=cos-to-sql--sm -ibmcloud resource service-instance-create $SM_INSTANCE_NAME secrets-manager 7713c3a8-3be8-4a9a-81bb-ee822fcaac3d ${REGION} -p \ -'{ - "allowed_network": "public-and-private" -}' - -SM_INSTANCE_ID=$(ibmcloud resource service-instance $SM_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .id') -SM_INSTANCE_GUID=$(ibmcloud resource service-instance $SM_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .guid') -SECRETS_MANAGER_URL=https://${SM_INSTANCE_GUID}.${REGION}.secrets-manager.appdomain.cloud -SECRETS_MANAGER_URL_PRIVATE=https://${SM_INSTANCE_GUID}.private.${REGION}.secrets-manager.appdomain.cloud -``` + ``` + DB_INSTANCE_NAME=cos-to-sql--pg + ibmcloud resource service-instance-create $DB_INSTANCE_NAME databases-for-postgresql standard ${REGION} --service-endpoints private -p \ + '{ + "disk_encryption_instance_crn": "none", + "disk_encryption_key_crn": "none", + "members_cpu_allocation_count": "0 cores", + "members_disk_allocation_mb": "10240MB", + "members_host_flavor": "multitenant", + "members_members_allocation_count": 2, + "members_memory_allocation_mb": "8192MB", + "service-endpoints": "private", + "version": "16" + }' + + DB_INSTANCE_ID=$(ibmcloud resource service-instance $DB_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .id') + ``` + +* Create the Secrets Manager instance. **Note:** To be able to create secret through the CLI running on your local workstation, we are creating the SecretsManager instance with private and public endpoints enabled. For production use, we strongly recommend to specify `allowed_network: private-only` + ``` + SM_INSTANCE_NAME=cos-to-sql--sm + ibmcloud resource service-instance-create $SM_INSTANCE_NAME secrets-manager 7713c3a8-3be8-4a9a-81bb-ee822fcaac3d ${REGION} -p \ + '{ + "allowed_network": "public-and-private" + }' + + SM_INSTANCE_ID=$(ibmcloud resource service-instance $SM_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .id') + SM_INSTANCE_GUID=$(ibmcloud resource service-instance $SM_INSTANCE_NAME --location ${REGION} --output json | jq -r '.[0] | .guid') + SECRETS_MANAGER_URL_PRIVATE=https://${SM_INSTANCE_GUID}.private.${REGION}.secrets-manager.appdomain.cloud + ``` * Create a S2S policy "Key Manager" between SM and the DB -``` -ibmcloud iam authorization-policy-create secrets-manager databases-for-postgresql \ - "Key Manager" \ - --source-service-instance-id $SM_INSTANCE_ID \ - --target-service-instance-id $DB_INSTANCE_ID -``` + ``` + ibmcloud iam authorization-policy-create secrets-manager databases-for-postgresql \ + "Key Manager" \ + --source-service-instance-id $SM_INSTANCE_ID \ + --target-service-instance-id $DB_INSTANCE_ID + ``` * Create the service credential to access the PostgreSQL instance -``` -SM_SECRET_FOR_PG_NAME=pg-access-credentials -ibmcloud secrets-manager secret-create \ - --secret-type="service_credentials" \ - --secret-name="$SM_SECRET_FOR_PG_NAME" \ - --secret-source-service="{\"instance\": {\"crn\": \"$DB_INSTANCE_ID\"},\"parameters\": {},\"role\": {\"crn\": \"crn:v1:bluemix:public:iam::::serviceRole:Writer\"}}" - -SM_SECRET_FOR_PG_ID=$(ibmcloud sm secret-by-name --name $SM_SECRET_FOR_PG_NAME --secret-type service_credentials --secret-group-name default --output JSON|jq -r '.id') -``` + ``` + SM_SECRET_FOR_PG_NAME=pg-access-credentials + ibmcloud secrets-manager secret-create \ + --secret-type="service_credentials" \ + --secret-name="$SM_SECRET_FOR_PG_NAME" \ + --secret-source-service="{\"instance\": {\"crn\": \"$DB_INSTANCE_ID\"},\"parameters\": {},\"role\": {\"crn\": \"crn:v1:bluemix:public:iam::::serviceRole:Writer\"}}" + SM_SECRET_FOR_PG_ID=$(ibmcloud sm secret-by-name --name $SM_SECRET_FOR_PG_NAME --secret-type service_credentials --secret-group-name default --instance-id $SM_INSTANCE_GUID --region $REGION --output JSON|jq -r '.id') + ``` * Create the Code Engine app: -``` -CE_APP_NAME=csv-to-sql -TRUSTED_PROFILE_FOR_COS_NAME=cos-to-sql--ce-to-cos-access -TRUSTED_PROFILE_FOR_SM_NAME=cos-to-sql--ce-to-sm-access - -ibmcloud code-engine app create \ - --name ${CE_APP_NAME} \ - --build-source https://github.com/IBM/CodeEngine \ - --build-commit - --build-context-dir cos-to-sql/ \ - --trusted-profiles-enabled="true" \ - --probe-ready type=http \ - --probe-ready path=/readiness \ - --probe-ready interval=30 \ - --env COS_REGION=${REGION} \ - --env COS_TRUSTED_PROFILE_NAME=${TRUSTED_PROFILE_FOR_COS_NAME} \ - --env SM_TRUSTED_PROFILE_NAME=${TRUSTED_PROFILE_FOR_SM_NAME} \ - --env SM_SERVICE_URL=${SECRETS_MANAGER_URL_PRIVATE} \ - --env SM_PG_SECRET_ID=${SM_SECRET_FOR_PG_ID} -``` + ``` + CE_APP_NAME=csv-to-sql + TRUSTED_PROFILE_FOR_COS_NAME=cos-to-sql--ce-to-cos-access + TRUSTED_PROFILE_FOR_SM_NAME=cos-to-sql--ce-to-sm-access + + ibmcloud code-engine app create \ + --name ${CE_APP_NAME} \ + --build-source https://github.com/IBM/CodeEngine \ + --build-context-dir cos-to-sql/ \ + --trusted-profiles-enabled="true" \ + --probe-ready type=http \ + --probe-ready path=/readiness \ + --probe-ready interval=30 \ + --env COS_REGION=${REGION} \ + --env COS_TRUSTED_PROFILE_NAME=${TRUSTED_PROFILE_FOR_COS_NAME} \ + --env SM_TRUSTED_PROFILE_NAME=${TRUSTED_PROFILE_FOR_SM_NAME} \ + --env SM_SERVICE_URL=${SECRETS_MANAGER_URL_PRIVATE} \ + --env SM_PG_SECRET_ID=${SM_SECRET_FOR_PG_ID} + ``` ## Trusted Profile setup * Create a trusted profile that grants a Code Engine app access to your COS bucket -``` -ibmcloud iam trusted-profile-create ${TRUSTED_PROFILE_FOR_COS_NAME} -ibmcloud iam trusted-profile-link-create ${TRUSTED_PROFILE_FOR_COS_NAME} \ - --name ce-app-${CE_APP_NAME} \ - --cr-type CE --link-crn ${CE_INSTANCE_ID} \ - --link-component-type application \ - --link-component-name ${CE_APP_NAME} -ibmcloud iam trusted-profile-policy-create ${TRUSTED_PROFILE_FOR_COS_NAME} \ - --roles "Content Reader" \ - --service-name cloud-object-storage \ - --service-instance ${COS_INSTANCE_ID} \ - --resource-type bucket \ - --resource ${COS_BUCKET_NAME} -``` + ``` + ibmcloud iam trusted-profile-create ${TRUSTED_PROFILE_FOR_COS_NAME} + ibmcloud iam trusted-profile-link-create ${TRUSTED_PROFILE_FOR_COS_NAME} \ + --name ce-app-${CE_APP_NAME} \ + --cr-type CE --link-crn ${CE_INSTANCE_ID} \ + --link-component-type application \ + --link-component-name ${CE_APP_NAME} + ibmcloud iam trusted-profile-policy-create ${TRUSTED_PROFILE_FOR_COS_NAME} \ + --roles "Content Reader" \ + --service-name cloud-object-storage \ + --service-instance ${COS_INSTANCE_ID} \ + --resource-type bucket \ + --resource ${COS_BUCKET_NAME} + ``` * Create the trusted profile to access Secrets Manager -``` -ibmcloud iam trusted-profile-create ${TRUSTED_PROFILE_FOR_SM_NAME} -ibmcloud iam trusted-profile-link-create ${TRUSTED_PROFILE_FOR_SM_NAME} \ - --name ce-app-${CE_APP_NAME} \ - --cr-type CE --link-crn ${CE_INSTANCE_ID} \ - --link-component-type application \ - --link-component-name ${CE_APP_NAME} -ibmcloud iam trusted-profile-policy-create ${TRUSTED_PROFILE_FOR_SM_NAME} \ - --roles "SecretsReader" \ - --service-name secrets-manager \ - --service-instance ${SM_INSTANCE_ID} -``` + ``` + ibmcloud iam trusted-profile-create ${TRUSTED_PROFILE_FOR_SM_NAME} + ibmcloud iam trusted-profile-link-create ${TRUSTED_PROFILE_FOR_SM_NAME} \ + --name ce-app-${CE_APP_NAME} \ + --cr-type CE --link-crn ${CE_INSTANCE_ID} \ + --link-component-type application \ + --link-component-name ${CE_APP_NAME} + ibmcloud iam trusted-profile-policy-create ${TRUSTED_PROFILE_FOR_SM_NAME} \ + --roles "SecretsReader" \ + --service-name secrets-manager \ + --service-instance ${SM_INSTANCE_ID} + ``` ## Setting up eventing * Create an authorization policy to allow the Code Engine project receive events from COS: -``` -ibmcloud iam authorization-policy-create codeengine cloud-object-storage \ - "Notifications Manager" \ - --source-service-instance-id ${CE_INSTANCE_ID} \ - --target-service-instance-id ${COS_INSTANCE_ID} -``` + ``` + ibmcloud iam authorization-policy-create codeengine cloud-object-storage \ + "Notifications Manager" \ + --source-service-instance-id ${CE_INSTANCE_ID} \ + --target-service-instance-id ${COS_INSTANCE_ID} + ``` * Create the subscription for COS events of type "write": -``` -ibmcloud ce sub cos create \ - --name "coswatch-${CE_APP_NAME}" \ - --bucket ${COS_BUCKET_NAME} \ - --event-type "write" \ - --destination ${CE_APP_NAME} \ - --destination-type app \ - --path /cos-to-sql -``` + ``` + ibmcloud ce sub cos create \ + --name "coswatch-${CE_APP_NAME}" \ + --bucket ${COS_BUCKET_NAME} \ + --event-type "write" \ + --destination ${CE_APP_NAME} \ + --destination-type app \ + --path /cos-to-sql + ``` ## Verify the solution * Upload a CSV file to COS, to initate an event that leads to a job execution: -``` -curl -s https://raw.githubusercontent.com/IBM/CodeEngine/main/cos-to-sql/samples/users.csv > CodeEngine-sample-users.csv + ``` + curl -s https://raw.githubusercontent.com/IBM/CodeEngine/main/cos-to-sql/samples/users.csv > CodeEngine-sample-users.csv -cat CodeEngine-sample-users.csv + cat CodeEngine-sample-users.csv -ibmcloud cos object-put \ - --bucket ${COS_BUCKET_NAME} \ - --key users.csv \ - --body ./CodeEngine-sample-users.csv \ - --content-type text/csv -``` + ibmcloud cos object-put \ + --bucket ${COS_BUCKET_NAME} \ + --key users.csv \ + --body ./CodeEngine-sample-users.csv \ + --content-type text/csv + ``` * Inspect the app execution by opening the logs: -``` -ibmcloud code-engine app logs \ - --name ${CE_APP_NAME} \ - --follow -``` + ``` + ibmcloud code-engine app logs \ + --name ${CE_APP_NAME} \ + --follow + ``` diff --git a/cos-to-sql/app.mjs b/cos-to-sql/app.mjs index 877e6e8f..bcf92bfb 100644 --- a/cos-to-sql/app.mjs +++ b/cos-to-sql/app.mjs @@ -78,7 +78,7 @@ router.get("/", async (req, res) => { // Readiness endpoint router.get("/readiness", async (req, res) => { console.log(`handling /readiness`); - if (!await stat("/var/run/secrets/codeengine.cloud.ibm.com/compute-resource-token/token")) { + if (!(await stat("/var/run/secrets/codeengine.cloud.ibm.com/compute-resource-token/token"))) { console.error("Mounting the trusted profile compute resource token is not enabled"); res.writeHead(500, { "Content-Type": "application/json" }); res.end('{"error": "Mounting the trusted profile compute resource token is not enabled"}'); @@ -140,29 +140,22 @@ router.post("/cos-to-sql", async (req, res) => { const pgClient = await getPgClient(secretsManager, smPgSecretId); - // - // Perform a single SQL insert statement per user + // + // Iterate through the list of users console.log(`Writing converted CSV data to the PostgreSQL database ...`); - const insertOperations = []; - users.forEach((userToAdd) => { - insertOperations.push(addUser(pgClient, userToAdd.Firstname, userToAdd.Lastname)); - }); - - // Wait for all SQL insert operations to finish - console.log(`Waiting for all SQL INSERT operations to finish ...`); - await Promise.all(insertOperations) - .then((results) => { - results.forEach((result, idx) => console.log(`Added ${JSON.stringify(users[idx])} -> ${JSON.stringify(result)}`)); - console.info("COMPLETED"); - return Promise.resolve(); - }) - .catch((err) => { - console.error("Failed to add users to the database", err); - console.info("FAILED"); - return Promise.reject(); - }); - - console.log(`All ${insertOperations?.length} insertions done!`); + let numberOfProcessedUsers = 0; + for (const userToAdd of users) { + try { + // Perform a single SQL insert statement per user + const result = await addUser(pgClient, userToAdd.Firstname, userToAdd.Lastname); + console.log(`Added ${JSON.stringify(userToAdd)} -> ${JSON.stringify(result)}`); + numberOfProcessedUsers++; + } catch (err) { + console.error(`Failed to add user '${JSON.stringify(userToAdd)}' to the database`, err); + } + }; + + console.log(`Processed ${numberOfProcessedUsers} user records!`); res.writeHead(200, { "Content-Type": "application/json" }); res.end(`{"status": "done"}`); return; diff --git a/cos-to-sql/utils/db.mjs b/cos-to-sql/utils/db.mjs index 03825853..c8924bf6 100644 --- a/cos-to-sql/utils/db.mjs +++ b/cos-to-sql/utils/db.mjs @@ -20,7 +20,7 @@ function connectDb(connectionString, caCert) { let pool = new Pool({ ...postgreConfig, max: 20, idleTimeoutMillis: 5000, connectionTimeoutMillis: 2000 }); pool.query( - "CREATE TABLE IF NOT EXISTS users (firstname varchar(256) NOT NULL, lastname varchar(256) NOT NULL)", + "CREATE TABLE IF NOT EXISTS users (firstname varchar(256) NOT NULL, lastname varchar(256) NOT NULL, CONSTRAINT pk_users PRIMARY KEY(firstname, lastname));", (err, result) => { if (err) { console.log(`Failed to create PostgreSQL table 'users'`, err); @@ -78,7 +78,7 @@ export function addUser(client, firstName, lastName) { const startTime = Date.now(); console.log(`${fn} > firstName: '${firstName}', lastName: '${lastName}'`); return new Promise(function (resolve, reject) { - const queryText = "INSERT INTO users(firstname,lastname) VALUES($1, $2)"; + const queryText = "INSERT INTO users(firstname,lastname) VALUES($1, $2) ON CONFLICT (firstname, lastname) DO NOTHING"; client.query(queryText, [firstName, lastName], function (error, result) { if (error) { console.log(`${fn} < failed - error: ${error}; duration ${Date.now() - startTime} ms`); @@ -111,7 +111,7 @@ export function deleteUsers(client) { const startTime = Date.now(); console.log(`${fn} >`); return new Promise(function (resolve, reject) { - const queryText = "DELETE FROM users"; + const queryText = "DROP TABLE users"; client.query(queryText, undefined, function (error, result) { if (error) { console.log(`${fn} < failed - error: ${error}; duration ${Date.now() - startTime} ms`); From e58c48eb2a9502bb5cd34aca0edd2407fe4f6d80 Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Thu, 17 Apr 2025 16:35:58 +0200 Subject: [PATCH 16/17] addressed review feedback and removed the port configuration --- cos-to-sql/app.mjs | 6 +++--- cos-to-sql/utils/db.mjs | 14 ++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/cos-to-sql/app.mjs b/cos-to-sql/app.mjs index bcf92bfb..ea9605d1 100644 --- a/cos-to-sql/app.mjs +++ b/cos-to-sql/app.mjs @@ -140,7 +140,7 @@ router.post("/cos-to-sql", async (req, res) => { const pgClient = await getPgClient(secretsManager, smPgSecretId); - // + // // Iterate through the list of users console.log(`Writing converted CSV data to the PostgreSQL database ...`); let numberOfProcessedUsers = 0; @@ -153,7 +153,7 @@ router.post("/cos-to-sql", async (req, res) => { } catch (err) { console.error(`Failed to add user '${JSON.stringify(userToAdd)}' to the database`, err); } - }; + } console.log(`Processed ${numberOfProcessedUsers} user records!`); res.writeHead(200, { "Content-Type": "application/json" }); @@ -174,7 +174,7 @@ router.get("/clear", async (req, res) => { }); // start server -const port = process.env.PORT || 8080; +const port = 8080; const server = app.listen(port, () => { console.log(`Server is up and running on port ${port}!`); }); diff --git a/cos-to-sql/utils/db.mjs b/cos-to-sql/utils/db.mjs index c8924bf6..9ccbb8c1 100644 --- a/cos-to-sql/utils/db.mjs +++ b/cos-to-sql/utils/db.mjs @@ -7,11 +7,15 @@ import pgConnectionString from "pg-connection-string"; let _pgPool; +/** + * Connect to PostgreSQL + * https://node-postgres.com/ + */ function connectDb(connectionString, caCert) { return new Promise((resolve, reject) => { const postgreConfig = pgConnectionString.parse(connectionString); - // Add some ssl + // Add the ssl ca cert postgreConfig.ssl = { ca: caCert, }; @@ -55,11 +59,8 @@ export async function getPgClient(secretsManager, secretId) { }); console.log(`Secret '${secretId}' fetched in ${Date.now() - startTime} ms`); - // - // Connect to PostgreSQL - // https://node-postgres.com/ console.log( - `Establishing connection to PostgreSQL database using SM secret '${res.result.name}' (last updated: '${res.result.updated_at}') ...` + `Connecting to the DB using SM secret '${res.result.name}' (last updated: '${res.result.updated_at}') ...` ); const pgCaCert = Buffer.from(res.result.credentials.connection.postgres.certificate.certificate_base64, "base64"); const pgConnectionString = res.result.credentials.connection.postgres.composed[0]; @@ -78,7 +79,8 @@ export function addUser(client, firstName, lastName) { const startTime = Date.now(); console.log(`${fn} > firstName: '${firstName}', lastName: '${lastName}'`); return new Promise(function (resolve, reject) { - const queryText = "INSERT INTO users(firstname,lastname) VALUES($1, $2) ON CONFLICT (firstname, lastname) DO NOTHING"; + const queryText = + "INSERT INTO users(firstname,lastname) VALUES($1, $2) ON CONFLICT (firstname, lastname) DO NOTHING"; client.query(queryText, [firstName, lastName], function (error, result) { if (error) { console.log(`${fn} < failed - error: ${error}; duration ${Date.now() - startTime} ms`); From b95212af2878dffb36512984c558b9b9ee11e6de Mon Sep 17 00:00:00 2001 From: Enrico Regge Date: Sat, 19 Apr 2025 08:27:35 +0200 Subject: [PATCH 17/17] Addressed review feedback --- cos-to-sql/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cos-to-sql/README.md b/cos-to-sql/README.md index 28461f9d..192779da 100644 --- a/cos-to-sql/README.md +++ b/cos-to-sql/README.md @@ -181,7 +181,7 @@ Install `jq`. On MacOS, you can use following [brew formulae](https://formulae.b * Upload a CSV file to COS, to initate an event that leads to a job execution: ``` - curl -s https://raw.githubusercontent.com/IBM/CodeEngine/main/cos-to-sql/samples/users.csv > CodeEngine-sample-users.csv + curl --silent --location --request GET 'https://raw.githubusercontent.com/IBM/CodeEngine/main/cos-to-sql/samples/users.csv' > CodeEngine-sample-users.csv cat CodeEngine-sample-users.csv