diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..7f9791d --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,98 @@ +name: Docker + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +on: + push: + branches: ["main"] + # Publish semver tags as releases. + tags: ["v*.*.*"] + pull_request: + branches: ["main"] + +# abort running jobs if newer version is detected on same branch +concurrency: + group: ${{ github.head_ref || github.ref }} + cancel-in-progress: true + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: ghcr.io + BASE_IMAGE_NAME: directus-flows + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: "true" + + # Install the cosign tool except on PR + # https://github.com/sigstore/cosign-installer + - name: Install cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@v3.3.0 + with: + cosign-release: "v2.2.3" + + # Workaround: https://github.com/docker/build-push-action/issues/461 + - name: Setup Docker buildx + uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }} + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a + with: + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + file: flows/Dockerfile + + # Sign the resulting Docker image digest except on PRs. + # This will only write to the public Rekor transparency log when the Docker + # repository is public to avoid leaking data. If you would like to publish + # transparency data even for private images, pass --force to cosign below. + # https://github.com/sigstore/cosign + - name: Sign the published Docker image + if: ${{ github.event_name != 'pull_request' }} + env: + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + # This step uses the identity token to provision an ephemeral certificate + # against the sigstore community Fulcio instance. + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9c32d7a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +directus-config +hooks/dist diff --git a/directus-config/collections/flows.json b/directus-config/collections/flows.json index be6bf70..fd672ef 100644 --- a/directus-config/collections/flows.json +++ b/directus-config/collections/flows.json @@ -118,6 +118,83 @@ "status": "active", "trigger": "event" }, + { + "_syncId": "60212b34-cb7a-4278-b484-98cff4d84933", + "accountability": "all", + "color": null, + "description": null, + "icon": "bolt", + "name": "Trigger TS Flows", + "operation": "090b7ffc-cd42-47f8-8d7a-5334db4d108a", + "options": { + "collections": [ + "association", + "association_poles", + "association_poles_translations", + "association_memberships", + "association_memberships_translations", + "association_public_files", + "association_public_files_translations", + "association_partners", + "association_social_links", + "association_translations", + "commissions", + "commission_memberships", + "commission_memberships_translations", + "commissions_social_links", + "commissions_translations", + "languages", + "members", + "news", + "news_partners", + "news_commissions", + "news_translations", + "partners", + "partner_category", + "partner_category_translations", + "social_links", + "events", + "registrations", + "clothes_orders", + "icbd_activities_registrations", + "inventory", + "galleries", + "save_the_date", + "save_the_date_translations", + "std_cell", + "std_cell_translations", + "subsonic", + "artists", + "subsonic_translations", + "icbd", + "icbd_files", + "icbd_translations", + "icbd_speakers", + "icbd_phds", + "icbd_activities", + "icbd_activities_translations", + "icbd_activities_icbd_speakers", + "game_star", + "game_star_translations", + "game_star_articles", + "game_star_articles_translations", + "game_star_events", + "game_star_events_translations", + "game_star_projects", + "game_star_social_links", + "game_star_projects_translations", + "clothes" + ], + "scope": [ + "items.create", + "items.update", + "items.delete" + ], + "type": "action" + }, + "status": "active", + "trigger": "event" + }, { "_syncId": "6c94bf7c-3939-4eca-8b5f-e199bd9022c3", "accountability": "all", @@ -305,26 +382,6 @@ "status": "active", "trigger": "event" }, - { - "_syncId": "d2ba770b-1958-4b19-bcd3-82193aac3a3d", - "accountability": "all", - "color": "#FF9D2D", - "description": "Updates activities timeslots to indicate whether they are full", - "icon": "lock_reset", - "name": "[ICBD] Update activity status", - "operation": "e5551dae-8ba2-4711-aa47-1ffe2f9a267e", - "options": { - "collections": [ - "icbd_activities_registrations" - ], - "scope": [ - "items.update" - ], - "type": "action" - }, - "status": "active", - "trigger": "event" - }, { "_syncId": "d408862a-d0fe-465a-817c-2ad99fdbe21b", "accountability": "all", diff --git a/directus-config/collections/operations.json b/directus-config/collections/operations.json index 4862846..133b9e9 100644 --- a/directus-config/collections/operations.json +++ b/directus-config/collections/operations.json @@ -56,24 +56,6 @@ "resolve": "56f961af-1a93-4e6e-8905-f572a1d425d2", "type": "exec" }, - { - "_syncId": "702df538-b576-4f29-8439-b3fed05c0f3d", - "flow": "d2ba770b-1958-4b19-bcd3-82193aac3a3d", - "key": "activity", - "name": "activity", - "options": { - "collection": "icbd_activities", - "key": [ - "{{activity_registration.icbd_activity}}" - ], - "permissions": "$full" - }, - "position_x": 37, - "position_y": 1, - "reject": null, - "resolve": "1756a6a2-dc5c-4f38-b21b-0f7c36f93671", - "type": "item-read" - }, { "_syncId": "ad18cf60-d72c-46e7-96ff-d6438de43397", "flow": "2eb347c0-164e-4a8b-a6f1-e4d66472389b", @@ -88,24 +70,6 @@ "resolve": "d4dd601e-b032-4d76-b1d9-c894f81b64d6", "type": "exec" }, - { - "_syncId": "e5551dae-8ba2-4711-aa47-1ffe2f9a267e", - "flow": "d2ba770b-1958-4b19-bcd3-82193aac3a3d", - "key": "activity_registration", - "name": "activity_registration", - "options": { - "collection": "icbd_activities_registrations", - "key": [ - "{{$trigger.keys[0]}}" - ], - "permissions": "$full" - }, - "position_x": 19, - "position_y": 1, - "reject": null, - "resolve": "702df538-b576-4f29-8439-b3fed05c0f3d", - "type": "item-read" - }, { "_syncId": "e651358b-6c57-408c-8729-978d0fa6eba2", "flow": "2eb347c0-164e-4a8b-a6f1-e4d66472389b", @@ -444,8 +408,8 @@ "type": "condition" }, { - "_syncId": "946f0eab-853f-499e-8fce-df865e7a1b75", - "flow": "48c5e220-ea71-485a-b86f-dbe5b7bebd18", + "_syncId": "a23d260f-4426-405a-8d9b-9611f378cdec", + "flow": "974bb3ab-5db4-48b3-88c8-16b80b94f5cc", "key": "condition_vx4p9", "name": "Condition", "options": { @@ -457,15 +421,15 @@ } } }, - "position_x": 37, + "position_x": 55, "position_y": 1, "reject": null, - "resolve": "2e33f74a-6b32-4653-9e88-15d1a22a8c81", + "resolve": "bb2ed939-f05d-4d02-bf24-7886285ce693", "type": "condition" }, { - "_syncId": "a23d260f-4426-405a-8d9b-9611f378cdec", - "flow": "974bb3ab-5db4-48b3-88c8-16b80b94f5cc", + "_syncId": "946f0eab-853f-499e-8fce-df865e7a1b75", + "flow": "48c5e220-ea71-485a-b86f-dbe5b7bebd18", "key": "condition_vx4p9", "name": "Condition", "options": { @@ -477,10 +441,10 @@ } } }, - "position_x": 55, + "position_x": 37, "position_y": 1, "reject": null, - "resolve": "bb2ed939-f05d-4d02-bf24-7886285ce693", + "resolve": "2e33f74a-6b32-4653-9e88-15d1a22a8c81", "type": "condition" }, { @@ -553,6 +517,44 @@ "resolve": "571d0635-9791-4779-9bf0-43a7e0aa0589", "type": "exec" }, + { + "_syncId": "3076f715-a8d5-4072-a71d-d5ddef11c0e8", + "flow": "d408862a-d0fe-465a-817c-2ad99fdbe21b", + "key": "event", + "name": "Event", + "options": { + "collection": "events", + "emitEvents": false, + "key": [ + "{{order.event}}" + ], + "permissions": "$trigger" + }, + "position_x": 19, + "position_y": 17, + "reject": null, + "resolve": "2fae799f-f084-4ed3-a850-f88546bc2a68", + "type": "item-read" + }, + { + "_syncId": "68dd5e2e-70d2-4499-8091-0aa471f2cd99", + "flow": "974bb3ab-5db4-48b3-88c8-16b80b94f5cc", + "key": "event", + "name": "Read Data", + "options": { + "collection": "events", + "emitEvents": false, + "key": [ + "{{registration.event}}" + ], + "permissions": "$trigger" + }, + "position_x": 37, + "position_y": 1, + "reject": null, + "resolve": "a23d260f-4426-405a-8d9b-9611f378cdec", + "type": "item-read" + }, { "_syncId": "09e288c7-ba9e-43eb-b02d-d3ad03000505", "flow": "6cf86bff-9bfb-4707-9166-411d7b60f564", @@ -571,20 +573,20 @@ "type": "item-read" }, { - "_syncId": "ed83b9a5-3cef-42c2-bb3d-63cf19062047", - "flow": "6c94bf7c-3939-4eca-8b5f-e199bd9022c3", + "_syncId": "0a58b6c8-bcb0-428c-9167-4f180efb9bdf", + "flow": "2eb347c0-164e-4a8b-a6f1-e4d66472389b", "key": "event", "name": "Event", "options": { "collection": "events", "key": [ - "{{$trigger.payload.event}}" + "{{registration.event}}" ] }, - "position_x": 19, + "position_x": 55, "position_y": 1, "reject": null, - "resolve": "ee33be1e-8718-4005-be5f-42cbe42b619d", + "resolve": "911d7c91-d0b8-426b-a158-ec4fdf22a367", "type": "item-read" }, { @@ -609,41 +611,20 @@ "type": "item-read" }, { - "_syncId": "cb968434-91cc-4152-a02b-60e9e527a010", - "flow": "36aad668-c920-4804-9cde-be8f0e89a947", - "key": "event", - "name": "Read Event", - "options": { - "collection": "events", - "emitEvents": false, - "key": [ - "{{order.event}}" - ], - "permissions": "$trigger" - }, - "position_x": 19, - "position_y": 17, - "reject": null, - "resolve": "05cdb8ec-815d-4e6e-90b8-3cfd44df0b69", - "type": "item-read" - }, - { - "_syncId": "3076f715-a8d5-4072-a71d-d5ddef11c0e8", - "flow": "d408862a-d0fe-465a-817c-2ad99fdbe21b", + "_syncId": "06abeb8d-adf8-497c-8ffb-1374d366fd84", + "flow": "9a85d36a-6cde-4a6a-8b35-dbe798226592", "key": "event", "name": "Event", "options": { "collection": "events", - "emitEvents": false, "key": [ - "{{order.event}}" - ], - "permissions": "$trigger" + "{{$trigger.payload.event}}" + ] }, "position_x": 19, - "position_y": 17, + "position_y": 1, "reject": null, - "resolve": "2fae799f-f084-4ed3-a850-f88546bc2a68", + "resolve": "4ca0de06-11bb-4bd8-a121-f94c76ef5c88", "type": "item-read" }, { @@ -665,40 +646,6 @@ "resolve": "946f0eab-853f-499e-8fce-df865e7a1b75", "type": "item-read" }, - { - "_syncId": "06abeb8d-adf8-497c-8ffb-1374d366fd84", - "flow": "9a85d36a-6cde-4a6a-8b35-dbe798226592", - "key": "event", - "name": "Event", - "options": { - "collection": "events", - "key": [ - "{{$trigger.payload.event}}" - ] - }, - "position_x": 19, - "position_y": 1, - "reject": null, - "resolve": "4ca0de06-11bb-4bd8-a121-f94c76ef5c88", - "type": "item-read" - }, - { - "_syncId": "0a58b6c8-bcb0-428c-9167-4f180efb9bdf", - "flow": "2eb347c0-164e-4a8b-a6f1-e4d66472389b", - "key": "event", - "name": "Event", - "options": { - "collection": "events", - "key": [ - "{{registration.event}}" - ] - }, - "position_x": 55, - "position_y": 1, - "reject": null, - "resolve": "911d7c91-d0b8-426b-a158-ec4fdf22a367", - "type": "item-read" - }, { "_syncId": "340bc06c-947d-46f2-82d0-a8862d4a3563", "flow": "5dabf92a-fba4-44ba-ba31-03d5a55ffc13", @@ -734,22 +681,39 @@ "type": "item-read" }, { - "_syncId": "68dd5e2e-70d2-4499-8091-0aa471f2cd99", - "flow": "974bb3ab-5db4-48b3-88c8-16b80b94f5cc", + "_syncId": "cb968434-91cc-4152-a02b-60e9e527a010", + "flow": "36aad668-c920-4804-9cde-be8f0e89a947", "key": "event", - "name": "Read Data", + "name": "Read Event", "options": { "collection": "events", "emitEvents": false, "key": [ - "{{registration.event}}" + "{{order.event}}" ], "permissions": "$trigger" }, - "position_x": 37, + "position_x": 19, + "position_y": 17, + "reject": null, + "resolve": "05cdb8ec-815d-4e6e-90b8-3cfd44df0b69", + "type": "item-read" + }, + { + "_syncId": "ed83b9a5-3cef-42c2-bb3d-63cf19062047", + "flow": "6c94bf7c-3939-4eca-8b5f-e199bd9022c3", + "key": "event", + "name": "Event", + "options": { + "collection": "events", + "key": [ + "{{$trigger.payload.event}}" + ] + }, + "position_x": 19, "position_y": 1, "reject": null, - "resolve": "a23d260f-4426-405a-8d9b-9611f378cdec", + "resolve": "ee33be1e-8718-4005-be5f-42cbe42b619d", "type": "item-read" }, { @@ -1021,28 +985,6 @@ "resolve": null, "type": "item-update" }, - { - "_syncId": "e4ff4105-d960-4053-80b8-39d8b24ed3fe", - "flow": "d2ba770b-1958-4b19-bcd3-82193aac3a3d", - "key": "item_update_wrcj6", - "name": "Update Data", - "options": { - "collection": "icbd_activities", - "emitEvents": false, - "key": [ - "{{activity.id}}" - ], - "payload": { - "timeslots": "{{ timeslots }}" - }, - "permissions": "$full" - }, - "position_x": 91, - "position_y": 1, - "reject": null, - "resolve": null, - "type": "item-update" - }, { "_syncId": "7b15317e-2bb4-400a-9ffb-5a56f876425d", "flow": "a9a5e703-de12-4e18-9e71-987bd759ab88", @@ -1263,33 +1205,6 @@ "resolve": "486a60ce-ad9d-4b9c-94bd-a576600685b4", "type": "mail" }, - { - "_syncId": "1756a6a2-dc5c-4f38-b21b-0f7c36f93671", - "flow": "d2ba770b-1958-4b19-bcd3-82193aac3a3d", - "key": "matching_registrations", - "name": "matching_registrations", - "options": { - "collection": "icbd_activities_registrations", - "emitEvents": false, - "permissions": "$full", - "query": { - "filter": { - "_and": [ - { - "icbd_activity": { - "_eq": "{{activity.id}}" - } - } - ] - } - } - }, - "position_x": 55, - "position_y": 1, - "reject": null, - "resolve": "bf95ba44-07c8-41f8-8c5e-ef25350e0375", - "type": "item-read" - }, { "_syncId": "d81a7e12-0339-4f70-8cce-99b5dcd63921", "flow": "36aad668-c920-4804-9cde-be8f0e89a947", @@ -1418,22 +1333,20 @@ "type": "exec" }, { - "_syncId": "4f7ad4ef-5995-41f5-ac25-cc5b30f75e52", - "flow": "974bb3ab-5db4-48b3-88c8-16b80b94f5cc", + "_syncId": "8f43479a-f524-4c52-ab0d-002ba60e5199", + "flow": "2eb347c0-164e-4a8b-a6f1-e4d66472389b", "key": "registration", "name": "Registration", "options": { "collection": "registrations", - "emitEvents": false, "key": [ - "{{$trigger.keys[0]}}" - ], - "permissions": "$trigger" + "{{$trigger.key}}" + ] }, "position_x": 19, "position_y": 1, "reject": null, - "resolve": "68dd5e2e-70d2-4499-8091-0aa471f2cd99", + "resolve": "e651358b-6c57-408c-8729-978d0fa6eba2", "type": "item-read" }, { @@ -1454,38 +1367,22 @@ "type": "item-read" }, { - "_syncId": "8f43479a-f524-4c52-ab0d-002ba60e5199", - "flow": "2eb347c0-164e-4a8b-a6f1-e4d66472389b", - "key": "registration", - "name": "Registration", - "options": { - "collection": "registrations", - "key": [ - "{{$trigger.key}}" - ] - }, - "position_x": 19, - "position_y": 1, - "reject": null, - "resolve": "e651358b-6c57-408c-8729-978d0fa6eba2", - "type": "item-read" - }, - { - "_syncId": "2936b8f7-a46f-4392-afd0-3d9974f4ad2d", - "flow": "5dabf92a-fba4-44ba-ba31-03d5a55ffc13", + "_syncId": "4f7ad4ef-5995-41f5-ac25-cc5b30f75e52", + "flow": "974bb3ab-5db4-48b3-88c8-16b80b94f5cc", "key": "registration", "name": "Registration", "options": { "collection": "registrations", + "emitEvents": false, "key": [ - "{{registration_key}}" + "{{$trigger.keys[0]}}" ], - "query": null + "permissions": "$trigger" }, - "position_x": 37, + "position_x": 19, "position_y": 1, "reject": null, - "resolve": "33ad2b75-22bb-4cd4-9bcf-7d4885fb4955", + "resolve": "68dd5e2e-70d2-4499-8091-0aa471f2cd99", "type": "item-read" }, { @@ -1523,6 +1420,24 @@ "resolve": "243a2be3-5cc0-431a-a3bd-290a25d2b46b", "type": "item-read" }, + { + "_syncId": "2936b8f7-a46f-4392-afd0-3d9974f4ad2d", + "flow": "5dabf92a-fba4-44ba-ba31-03d5a55ffc13", + "key": "registration", + "name": "Registration", + "options": { + "collection": "registrations", + "key": [ + "{{registration_key}}" + ], + "query": null + }, + "position_x": 37, + "position_y": 1, + "reject": null, + "resolve": "33ad2b75-22bb-4cd4-9bcf-7d4885fb4955", + "type": "item-read" + }, { "_syncId": "a07ae0a2-4b49-468d-ba22-2a248b209301", "flow": "5dabf92a-fba4-44ba-ba31-03d5a55ffc13", @@ -1538,48 +1453,48 @@ "type": "exec" }, { - "_syncId": "94fb122e-881f-49aa-96b9-025a8455cfd2", - "flow": "6cf86bff-9bfb-4707-9166-411d7b60f564", + "_syncId": "33ad2b75-22bb-4cd4-9bcf-7d4885fb4955", + "flow": "5dabf92a-fba4-44ba-ba31-03d5a55ffc13", "key": "registrations", - "name": "Fetch registrations", + "name": "Registrations", "options": { "collection": "registrations", - "emitEvents": false, - "permissions": "$trigger", "query": { "filter": { "event": { - "_eq": "{{$trigger.event}}" + "_eq": "{{registration.event}}" } - }, - "limit": 4096 + } } }, - "position_x": 19, + "position_x": 55, "position_y": 1, "reject": null, - "resolve": "09e288c7-ba9e-43eb-b02d-d3ad03000505", + "resolve": "340bc06c-947d-46f2-82d0-a8862d4a3563", "type": "item-read" }, { - "_syncId": "33ad2b75-22bb-4cd4-9bcf-7d4885fb4955", - "flow": "5dabf92a-fba4-44ba-ba31-03d5a55ffc13", + "_syncId": "94fb122e-881f-49aa-96b9-025a8455cfd2", + "flow": "6cf86bff-9bfb-4707-9166-411d7b60f564", "key": "registrations", - "name": "Registrations", + "name": "Fetch registrations", "options": { "collection": "registrations", + "emitEvents": false, + "permissions": "$trigger", "query": { "filter": { "event": { - "_eq": "{{registration.event}}" + "_eq": "{{$trigger.event}}" } - } + }, + "limit": 4096 } }, - "position_x": 55, + "position_x": 19, "position_y": 1, "reject": null, - "resolve": "340bc06c-947d-46f2-82d0-a8862d4a3563", + "resolve": "09e288c7-ba9e-43eb-b02d-d3ad03000505", "type": "item-read" }, { @@ -1604,6 +1519,28 @@ "resolve": "889a4969-1f6c-43bb-b8a8-407df5626728", "type": "request" }, + { + "_syncId": "090b7ffc-cd42-47f8-8d7a-5334db4d108a", + "flow": "60212b34-cb7a-4278-b484-98cff4d84933", + "key": "request_sklql", + "name": "Webhook / Request URL", + "options": { + "body": "{\"payload\": {{$last}}, \"accountability\": {{$accountability}}}", + "headers": [ + { + "header": "Content-Type", + "value": "application/json" + } + ], + "method": "POST", + "url": "http://flows:3000" + }, + "position_x": 19, + "position_y": 1, + "reject": null, + "resolve": null, + "type": "request" + }, { "_syncId": "0a602f39-11f6-4418-a14a-e1605609b0c1", "flow": "9a85d36a-6cde-4a6a-8b35-dbe798226592", @@ -1628,20 +1565,6 @@ "resolve": "2a6b54fd-c404-4b7d-a6d6-d28ac88ac601", "type": "mail" }, - { - "_syncId": "bf95ba44-07c8-41f8-8c5e-ef25350e0375", - "flow": "d2ba770b-1958-4b19-bcd3-82193aac3a3d", - "key": "timeslots", - "name": "Compute fullness", - "options": { - "code": "module.exports = async function(data) {\n\treturn data.activity.timeslots.map((t) => {\n let assigned = data.matching_registrations.filter(\n (r) => r.start === t.start_time\n ).length;\n\n return { ...t, full: t.max_attendees <= assigned };\n });\n}" - }, - "position_x": 73, - "position_y": 1, - "reject": null, - "resolve": "e4ff4105-d960-4053-80b8-39d8b24ed3fe", - "type": "exec" - }, { "_syncId": "1411b62c-1d8d-4f32-b194-34c239d4d6d3", "flow": "2eb347c0-164e-4a8b-a6f1-e4d66472389b", diff --git a/directus.yaml b/directus.yaml index 307406b..158eec9 100644 --- a/directus.yaml +++ b/directus.yaml @@ -1,4 +1,23 @@ services: + flows: + image: node:22-alpine + working_dir: /app/flows + hostname: flows + user: ${UID:-1000} + volumes: + - ${PWD}:/app + entrypoint: sh -c "npm i && npm run dev" + environment: + DIRECTUS_URL: http://directus:8055 + DIRECTUS_TOKEN: 00000000-0000-0000-0000-000000000000 + + SMTP_HOST: mail.epfl.ch + SMTP_PORT: "587" + SMTP_USER: it.clic + SMTP_PASSWORD: ${SMTP_IT_PASSWORD} + SMTP_SECURE: "false" + SMTP_FROM: CLIC + # Directus service directus: labels: diff --git a/flows/.dockerignore b/flows/.dockerignore new file mode 100644 index 0000000..b21bde1 --- /dev/null +++ b/flows/.dockerignore @@ -0,0 +1,3 @@ +dist +node_modules +.gitignore diff --git a/flows/.gitignore b/flows/.gitignore new file mode 100644 index 0000000..76add87 --- /dev/null +++ b/flows/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/flows/Dockerfile b/flows/Dockerfile new file mode 100644 index 0000000..596d073 --- /dev/null +++ b/flows/Dockerfile @@ -0,0 +1,18 @@ +# Must be built with the whole repository as context to be able to access the Directus' schema. + +FROM node:22-alpine + +WORKDIR /app + +COPY flows/package.json flows/package-lock.json ./ +RUN npm ci --frozen + +COPY flows/tsconfig.json ./ +COPY flows/src ./src +RUN rm src/types +COPY types ./src/types + +RUN npx tsc --project tsconfig.json + +CMD ["node", "dist/main.js"] + diff --git a/flows/README.md b/flows/README.md new file mode 100644 index 0000000..b14c0f8 --- /dev/null +++ b/flows/README.md @@ -0,0 +1,45 @@ +# Directus flows + +This folder contains a NodeJS webserver that is able to process database updates, based on the [Flows](https://directus.io/docs/guides/automate/flows) feature of Directus. Its main goal is to replace said Flows on Directus by flows written in Typescript to ease development, review and testing. + +## Design + +The webserver consists of a single route on `/` that waits for `POST` requests with the body described by the [`DirectusMessage`](src/directus.ts) type. There is no authentication on this route, as it's meant to be available only in the Directus' instance dedicated Docker stack. Directus contains a single flow that is triggered by any change in the database (create, update or delete) and that simply calls this webserver and forwards the trigger's data. + +When the webserver receives a message, it dispatches to all `Flow`s registered to this particular event (see the [`registerFlow`](src/registry.ts) function). `Flow`s are basically handler functions, taking as parameter the event and some extra utilities. For convenience, Typescript definitions for all the events are available in [src/directus.ts](src/directus.ts). All files registering flows should be placed within the [`src/flows`](src/flows) directus, or in subfolders. + +### Utilities + +Flow handler receive the following utilities objects/methods: + +- `fetch/update` methods: These two methods allow to easily read or update the element(s) affected by the event. Note that `fetch` is only available for the `Update` event as the `Create` one has the full object in the payload. The `Delete` event has no associated function since the elements are already removed from the database and therefore cannot be read. + + **Note:** these methods are stored in the `event` rather than the `utils` parameter of the handler, this is because they need the same generic arguments as the event, and adding this genericity to `HandlerUtils` would probably make the flows more messy to write. This may be an improvement for later. + +- Mailer: this object exposes a single `sendMail` function, which does as its name says. +- Directus: handle to directly access the directus instance to read/update/delete entries. + +### TTL + +An issue that may arise from updating Directus from within a flow is that it could cause and infinite loop of updates and flow trigger, for example if a flow always updates the entry it was triggered from. To circumvent this, a TTL is implemented through the `User-Agent` header used when the Directus handle (see above) accesses the instance. The header is set to a base and is appended a decrementing integer (see the `DIRECTUS_AGENT_BASE` and `DIRECTUS_AGENT_TTL` configuration variable below), and if an event arrives with a TTL of zero then it is ignored. However this is merely a failsafe as flows should normally never get into such a loop. + +## Configuration + +| Name | Default value | Required | Description | +| --------------------- | ------------------- | -------- | ---------------------------------------------------------------------------------------------------- | +| `LOG_LEVEL` | info | No | Level of lots to display | +| `PORT` | 3000 | No | Port on which the server will listen | +| `DIRECTUS_URL` | | Yes | URL to access the Directus instance issuing the events. | +| `DIRECTUS_TOKEN` | | No | Access token to interact with Directus. If not specified, access is done through the `public` role. | +| `DIRECTUS_AGENT_BASE` | `"directus-flows-"` | No | Start of the `User-Agent` header when interacting with Directus (see [TTL](#ttl)). | +| `DIRECTUS_AGENT_TTL` | 5 | No | Number of consecutive triggers allowed (see [TTL](#ttl)). | +| `SMTP_HOST` | | Yes | Hostname for the SMTP server. | +| `SMTP_PORT` | 587 | No | Port to access the SMTP server. | +| `SMTP_SECURE` | true | No | If set to false, attemps to communicate with the SMTP server using STARTTLS, otherwise uses SSL/TLS. | +| `SMTP_USER` | | Yes | Username for SMTP login. | +| `SMTP_PASSWORD` | | Yes | Password for SMTP login. | +| `SMTP_FROM` | | Yes | `from` field for sent emails. | + +## Build + +The webserver is built into a Docker image, but it must be noted that the [`Dockerfile`](./Dockerfile) expects to be built at the root of the repository, as it needs to access the Directus' schema stored in [`../types/`](../types/). diff --git a/flows/package-lock.json b/flows/package-lock.json new file mode 100644 index 0000000..b4e4cc4 --- /dev/null +++ b/flows/package-lock.json @@ -0,0 +1,614 @@ +{ + "name": "flows", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "flows", + "dependencies": { + "@directus/sdk": "^21.2.2", + "@types/nodemailer": "^7.0.11", + "nodemailer": "^8.0.4", + "winston": "^3.19.0" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "tsc-watch": "^7.2.0", + "tsconfig-paths": "^4.2.0", + "typescript": "^6.0.2" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@directus/sdk": { + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@directus/sdk/-/sdk-21.2.2.tgz", + "integrity": "sha512-T8UhDG+GY534AqvLpRmycLYZmIYIaUYzu25X3WeUjWuxCvqAiKqXbYUrTRb0A5Mw93vUeVuyqabzvLbBY9AXxQ==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/directus/directus?sponsor=1" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz", + "integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true, + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", + "dev": true + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/node-cleanup": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz", + "integrity": "sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemailer": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "license": [ + "MIT", + "Apache2" + ], + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/ps-tree": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", + "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-stream": "=3.3.4" + }, + "bin": { + "ps-tree": "bin/ps-tree.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/tsc-watch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/tsc-watch/-/tsc-watch-7.2.0.tgz", + "integrity": "sha512-4gRFawQD1cVSaILvG7wl2x6NtteKbS2dGBMbL7Q6n1ldLIOKXCJUoEwUXdGuee4dp+zcnA6tukBBLz1lZrNI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6", + "node-cleanup": "^2.1.2", + "ps-tree": "^1.2.0", + "string-argv": "^0.3.2" + }, + "bin": { + "tsc-watch": "dist/lib/tsc-watch.js" + }, + "engines": { + "node": ">=12.12.0" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + } + } +} diff --git a/flows/package.json b/flows/package.json new file mode 100644 index 0000000..9b25d4e --- /dev/null +++ b/flows/package.json @@ -0,0 +1,18 @@ +{ + "devDependencies": { + "@types/node": "^25.5.0", + "tsc-watch": "^7.2.0", + "tsconfig-paths": "^4.2.0", + "typescript": "^6.0.2" + }, + "scripts": { + "dev": "tsc-watch --preserveWatchOutput --onSuccess \"node dist/main.js\"" + }, + "type": "module", + "dependencies": { + "@directus/sdk": "^21.2.2", + "@types/nodemailer": "^7.0.11", + "nodemailer": "^8.0.4", + "winston": "^3.19.0" + } +} diff --git a/flows/src/directus.ts b/flows/src/directus.ts new file mode 100644 index 0000000..fb80ce7 --- /dev/null +++ b/flows/src/directus.ts @@ -0,0 +1,154 @@ +import { + createDirectus, + DirectusClient, + ReadItemOutput, + ReadSingletonOutput, + RegularCollections, + rest, + RestClient, + SingletonCollections, + staticToken, + UnpackList, + UpdateItemOutput, +} from "@directus/sdk"; +import { Schema } from "./types/schema.js"; +import { ENVS } from "./env.js"; + +type Flatten = T extends any[] ? T[number] : T; +type IdOf = T extends { id?: number } + ? number + : T extends { id?: string } + ? string + : never; +export type SchemaKey = keyof Schema; +export type EventType = "create" | "update" | "delete"; + +export type RawCreateEvent< + K extends SchemaKey, + Item = UnpackList, +> = { + event: `${K}.items.create`; + payload: Flatten; + key: IdOf>; + collection: K; +}; + +export type RawUpdateEventRegular< + K extends RegularCollections, + Item = UnpackList, +> = { + event: `${K}.items.update`; + payload: Flatten; + keys: string[]; + collection: K; +}; +export type RawUpdateEventSingleton< + K extends SingletonCollections, + Item = UnpackList, +> = { + event: `${K}.items.update`; + payload: Flatten; + key: IdOf; + collection: K; +}; +export type RawUpdateEvent = + K extends RegularCollections + ? RawUpdateEventRegular + : K extends SingletonCollections + ? RawUpdateEventSingleton + : never; + +export type CreateEvent> = { + update: (payload: Item) => Promise>; +} & RawCreateEvent; + +export type UpdateEventRegular< + K extends RegularCollections, + Item = UnpackList, +> = { + fetch: () => Promise[]>; + update: (payload: Item) => Promise[]>; +} & RawUpdateEventRegular; +export type UpdateEventSingleton< + K extends SingletonCollections, + Item = UnpackList, +> = { + fetch: () => Promise>; + update: (payload: Item) => Promise[]>; +} & RawUpdateEventSingleton; +export type UpdateEvent = + K extends RegularCollections + ? UpdateEventRegular + : K extends SingletonCollections + ? UpdateEventSingleton + : never; + +export type RawDeleteEvent = { + event: `${K}.items.delete`; + payload: string[]; + keys: string[]; + collection: K; +}; +export type DeleteEvent = RawDeleteEvent; + +export type DirectusEvent< + K extends SchemaKey, + T extends EventType, +> = T extends "create" + ? CreateEvent + : T extends "update" + ? UpdateEvent + : DeleteEvent; +export type RawDirectusEvent< + K extends SchemaKey, + T extends EventType, +> = T extends "create" + ? RawCreateEvent + : T extends "update" + ? RawUpdateEvent + : RawDeleteEvent; + +export type DirectusMessage = { + payload: RawDirectusEvent; + accountability: { + role: string; + user: string; + roles: string[]; + admin: boolean; + app: boolean; + ip: string; + userAgent: string; + origin: string; + session: string; + }; +}; + +export type Directus = DirectusClient & RestClient; + +/** Construct a `Directus` instance with an updated TTL, or null if TTL is 0. In that case, the event should not be processed. */ +export function makeDirectus(incomingAgent: string): Directus | null { + let agent = `${ENVS.directusAgentBase}${ENVS.directusAgentTtl}`; + + let m = incomingAgent.match(`${ENVS.directusAgentBase}(\\d+)`); + if (m) { + const ttl = parseInt(m[1]); + if (ttl <= 0) { + return null; + } + agent = `${ENVS.directusAgentBase}${ttl - 1}`; + } + + let directus = createDirectus(ENVS.directusUrl).with( + rest({ + onRequest: (r) => { + r.headers = { ...(r.headers || {}), "User-Agent": agent }; + return r; + }, + }), + ); + if (ENVS.directusToken) { + directus = directus.with(staticToken(ENVS.directusToken)); + } + + return directus; +} diff --git a/flows/src/env.ts b/flows/src/env.ts new file mode 100644 index 0000000..8d7f4e3 --- /dev/null +++ b/flows/src/env.ts @@ -0,0 +1,33 @@ +/** Get the environment variable with the given name, or null if it's not set. */ +function getEnvNullable(name: string): string | null { + return typeof process.env[name] === "string" ? process.env[name] : null; +} +/** Get the environment variable with the given name, or throw an error if it's not set. */ +function getEnv(name: string): string { + if (!(name in process.env)) { + throw Error(`Missing env ${name}`); + } + + return getEnvNullable(name)!; +} + +export const ENVS = { + logLevel: getEnvNullable("LOG_LEVEL") || "info", + + port: parseInt(getEnvNullable("PORT") || "3000"), + + directusUrl: getEnv("DIRECTUS_URL")!, + directusToken: getEnvNullable("DIRECTUS_TOKEN"), + directusAgentBase: + getEnvNullable(" DIRECTUS_AGENT_BASE") || "directus-flows-", + directusAgentTtl: parseInt(getEnvNullable(" DIRECTUS_AGENT_TTL") || "5"), + + smtp: { + host: getEnv("SMTP_HOST"), + port: parseInt(getEnvNullable("SMTP_PORT") || "587"), + secure: getEnvNullable("SMTP_SECURE") !== "false", + user: getEnv("SMTP_USER"), + password: getEnv("SMTP_PASSWORD"), + from: getEnv("SMTP_FROM"), + }, +}; diff --git a/flows/src/flows/icbd.ts b/flows/src/flows/icbd.ts new file mode 100644 index 0000000..244a181 --- /dev/null +++ b/flows/src/flows/icbd.ts @@ -0,0 +1,87 @@ +import { readItem, readItems, updateItem } from "@directus/sdk"; +import { DirectusEvent } from "../directus.js"; +import { HandlerUtils, registerFlow } from "../registry.js"; +import { ICBDActivity, ICBDActivityTimeslot } from "../types/aliases.js"; + +const updateActivityStatus = { + name: "Update ICBD activity status", + handler: async ( + e: DirectusEvent< + "icbd_activities_registrations", + "create" | "update" | "delete" + >, + { directus, logger }: HandlerUtils, + ) => { + let activities: ICBDActivity[] = []; + switch (e.event) { + case "icbd_activities_registrations.items.create": + activities = + typeof e.payload.icbd_activity === "number" + ? [ + await directus.request( + readItem("icbd_activities", e.payload.icbd_activity), + ), + ] + : []; + break; + case "icbd_activities_registrations.items.update": + const toFetch = [ + ...new Set((await e.fetch()).map((e) => e.icbd_activity!)), + ]; + activities = await directus.request( + readItems("icbd_activities", { filter: { id: { _in: toFetch } } }), + ); + case "icbd_activities_registrations.items.delete": + activities = await directus.request(readItems("icbd_activities")); + } + activities = activities.filter((a) => a.timeslots !== null); + + logger.info( + "Updating activities", + activities.map((a) => a.id), + ); + + for (const activity of activities) { + const other_registrations = await directus.request( + readItems("icbd_activities_registrations", { + filter: { icbd_activity: { _eq: activity.id } }, + }), + ); + + const timeslots = activity.timeslots as ICBDActivityTimeslot[]; + const new_timeslots = timeslots.map((t) => { + const assigned = other_registrations.filter( + (r) => r.start === t.start_time, + ).length; + const full = t.max_attendees <= assigned; + + if (full !== t.full) { + logger.info( + `Updating fullness of ${activity.id} (${t.start_time}) to ${full}`, + ); + } + + return { ...t, full }; + }); + + await directus.request( + updateItem("icbd_activities", activity.id!, { + timeslots: new_timeslots, + }), + ); + } + }, +}; + +registerFlow( + "icbd_activities_registrations.items.create", + updateActivityStatus, +); +registerFlow( + "icbd_activities_registrations.items.update", + updateActivityStatus, +); +registerFlow( + "icbd_activities_registrations.items.delete", + updateActivityStatus, +); diff --git a/flows/src/logger.ts b/flows/src/logger.ts new file mode 100644 index 0000000..cbda3c1 --- /dev/null +++ b/flows/src/logger.ts @@ -0,0 +1,30 @@ +import { + createLogger as createWinstonLogger, + transports, + format, + Logger as WinstonLogger, +} from "winston"; +import { ENVS } from "./env.js"; + +export type Logger = WinstonLogger; + +export default function createLogger(span: string): Logger { + return createWinstonLogger({ + level: ENVS.logLevel, + defaultMeta: { + span, + }, + format: format.combine( + format.errors({ stack: true }), + format.colorize(), + format.timestamp({ + format: 'hh:mm:ss', + + }), + format.printf(({ timestamp, level, span, message, stack }) => { + return `[${timestamp}][${span}] ${level}: ${message}${stack ? "\n" + stack : ""}`; + }), + ), + transports: [new transports.Console()], + }); +} diff --git a/flows/src/main.ts b/flows/src/main.ts new file mode 100644 index 0000000..0d96cc8 --- /dev/null +++ b/flows/src/main.ts @@ -0,0 +1,184 @@ +import { createServer } from "http"; +import { flows, Mailer } from "./registry.js"; +import { readdirSync } from "fs"; +import { join } from "path"; +import { + readItems, + readSingleton, + updateItem, + updateItems, + updateSingleton, +} from "@directus/sdk"; +import { + DirectusEvent, + DirectusMessage, + makeDirectus, + RawCreateEvent, + RawUpdateEvent, +} from "./directus.js"; +import { createTransport } from "nodemailer"; +import { ENVS } from "./env.js"; +import createLogger from "./logger.js"; + +const logger = createLogger("global"); + +// Import all files from the `dist/flows/` directory to run all `registerFlows` calls. +const files = readdirSync("./dist/flows/", { recursive: true }).filter((f) => + f.toString().endsWith(".js"), +); +for (const file of files) { + import(join(process.cwd(), "./dist/flows/", file.toString())); +} + +const nodemailer = createTransport({ + host: ENVS.smtp.host, + port: ENVS.smtp.port, + secure: ENVS.smtp.secure, // use STARTTLS (upgrade connection to TLS after connecting) + auth: { + user: ENVS.smtp.user, + pass: ENVS.smtp.password, + }, +}); +if (!(await nodemailer.verify())) { + throw new Error("Unable to connect to mail server"); +} + +// Construct the `Mailer` instance used by the flow. Using a wrapper allows to globally control some options of the mailer. +const mailer: Mailer = { + sendMail: async (opts) => { + // Use `env.STMP_FROM` as default. + return nodemailer.sendMail({ from: ENVS.smtp.from, ...opts }); + }, +}; + +// Server entry point. +const server = createServer(async (req, res) => { + let endWith = (code: number, message: string) => { + res.writeHead(code, message); + res.end(); + }; + const parsedURL = new URL(req.url ?? "", `http://${req.headers.host}`); + + // Healthcheck route. + if (req.method === "GET" && parsedURL.pathname === "/alive") { + endWith(200, "OK"); + return; + } + + // Request must be a POST on / with json data. + if (req.method !== "POST") { + endWith(405, "Method not allowed"); + return; + } + if (parsedURL.pathname !== "/") { + endWith(404, "Not Found"); + return; + } + if (req.headers["content-type"] !== "application/json") { + endWith(415, "Unsupported Media Type"); + return; + } + + // Read data into body. + let body = ""; + req.on("data", (chunk) => { + body += chunk.toString(); + }); + req.on("end", async () => { + try { + const message: DirectusMessage = JSON.parse(body); + + // Creates the directus handler, and returns an error if null (i.e. TTL is 0). + const directus = makeDirectus(message.accountability.userAgent); + if (!directus) { + endWith(429, "Too Many Requests"); + return; + } + + // Status of the different flows for this event. + const statuses = []; + + // Creates the functions to fetch/update the entries affected by the event. + let methods: any; + if (message.payload.event.endsWith(".create")) { + let create = message.payload as RawCreateEvent; + methods = { + async update(payload: any) { + return await directus.request( + updateItem(message.payload.collection, create.key, payload), + ); + }, + }; + } else if (message.payload.event.endsWith(".update")) { + let update = message.payload as RawUpdateEvent; + if ("keys" in update) { + methods = { + ...message, + async fetch() { + return await directus.request( + readItems(update.collection, { + filter: { id: { _in: update.keys } }, + }), + ); + }, + async update(payload: any) { + return await directus.request( + updateItems(update.collection, update.keys, payload), + ); + }, + }; + } else { + methods = { + ...message, + async fetch() { + return await directus.request(readSingleton(update.collection)); + }, + async update(payload: any) { + return await directus.request( + updateSingleton(update.collection, payload), + ); + }, + }; + } + } + + const event: DirectusEvent = { + ...message.payload, + ...methods, + } as DirectusEvent; + + if (event.event in flows) { + for (const flow of flows[event.event]) { + try { + await flow.handler(event, { + directus, + mailer, + logger: createLogger(flow.name), + }); + statuses.push(`${flow.name}: OK`); + } catch (e) { + statuses.push(`${flow.name}: ERR ${e}`); + } + } + } + + logger.info( + `[EVENT ${event.event}] ${statuses.length > 0 ? `\n${statuses.map((s) => " " + s).join("\n")}` : "No registered handler"}`, + ); + res.writeHead(200, "OK"); + res.end(); + } catch (error) { + logger.error(error); + endWith(422, "Unprocessable Content"); + } + }); +}); + +server.listen(ENVS.port, () => { + logger.info(`Server listening on port ${ENVS.port}`); + + logger.info("Registered handlers:"); + for (const event of Object.keys(flows)) { + logger.info(` ${event}: ${flows[event].map((f) => f.name).join(", ")}`); + } +}); diff --git a/flows/src/registry.ts b/flows/src/registry.ts new file mode 100644 index 0000000..8d0c23d --- /dev/null +++ b/flows/src/registry.ts @@ -0,0 +1,37 @@ +import { DirectusClient, RestClient } from "@directus/sdk"; +import { EventType, DirectusEvent, SchemaKey, Directus } from "./directus.js"; +import { Schema } from "./types/schema.js"; +import { SendMailOptions, SentMessageInfo, Transporter } from "nodemailer"; +import { Logger } from "./logger.js"; + +type EventHandler = ( + event: DirectusEvent, + utils: HandlerUtils, +) => Promise; +interface Flow { + name: string; + handler: EventHandler; +} + +export type HandlerUtils = { + directus: Directus; + mailer: Mailer; + logger: Logger; +}; + +export type Mailer = { + sendMail: (opts: SendMailOptions) => Promise; +}; + +export const flows: { [name: string]: Flow[] } = {}; + +export function registerFlow( + name: `${K}.items.${T}`, + handler: Flow, +) { + if (name in flows) { + flows[name].push(handler); + } else { + flows[name] = [handler]; + } +} diff --git a/flows/src/types b/flows/src/types new file mode 120000 index 0000000..8788aa2 --- /dev/null +++ b/flows/src/types @@ -0,0 +1 @@ +../../types \ No newline at end of file diff --git a/flows/tsconfig.json b/flows/tsconfig.json new file mode 100644 index 0000000..0bd8bec --- /dev/null +++ b/flows/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "types": ["node"], + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +} diff --git a/types/aliases.ts b/types/aliases.ts index ab71fbb..ab94764 100644 --- a/types/aliases.ts +++ b/types/aliases.ts @@ -1,11 +1,11 @@ -import { Schema, components } from "./schema"; +import { Schema, components } from "./schema.js"; // A few useful aliases to Directus' generated Typescript definitions. export type Association = components["schemas"]["ItemsAssociation"]; export type Subsonic = components["schemas"]["ItemsSubsonic"]; export type GameStar = components["schemas"]["ItemsGameStar"]; -export type SaveTheDate = components["schemas"]["ItemsSavetheDate"]; +export type SaveTheDate = components["schemas"]["ItemsSavetheDate"]; export type AssociationMembership = Schema["association_memberships"][0]; export type AssociationPartner = Schema["association_partners"][0]; @@ -25,10 +25,12 @@ export type ICBD = Schema["icbd"][0]; export type ICBDSpeaker = Schema["icbd_speakers"][0]; export type ICBDActivity = Schema["icbd_activities"][0]; export type ICBDPhd = Schema["icbd_phds"][0]; -export type ICBDSpeakerActivityRelation = Schema["icbd_activities_icbd_speakers"][0]; +export type ICBDSpeakerActivityRelation = + Schema["icbd_activities_icbd_speakers"][0]; export type Registration = Schema["registrations"][0]; export type Event = Schema["events"][0]; -export type ICBDActivityRegistration = Schema["icbd_activities_registrations"][0]; +export type ICBDActivityRegistration = + Schema["icbd_activities_registrations"][0]; export type GameStarArticle = Schema["game_star_articles"][0]; export type GameStarEvent = Schema["game_star_events"][0]; export type GameStarProject = Schema["game_star_projects"][0]; @@ -36,3 +38,11 @@ export type GameStarSocialLink = Schema["game_star_social_links"][0]; export type Gallery = Schema["galleries"][0]; export type Clothes = Schema["clothes"][0]; +export type ICBDActivityTimeslot = { + room: string; + start_time: string; + end_time: string; + custom_name: string; + max_attendees: number; + full: boolean; +};