@satorio/machinery 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/dictionary/all.json +4656 -0
- package/dictionary/all.toon +639 -0
- package/dictionary/core.json +221 -0
- package/dictionary/core.toon +33 -0
- package/dictionary/graphql.json +212 -0
- package/dictionary/graphql.toon +32 -0
- package/dictionary/http.json +113 -0
- package/dictionary/http.toon +21 -0
- package/dictionary/identifiers.json +266 -0
- package/dictionary/identifiers.toon +38 -0
- package/dictionary/index.json +96 -0
- package/dictionary/index.toon +18 -0
- package/dictionary/integration.json +131 -0
- package/dictionary/integration.toon +23 -0
- package/dictionary/money.json +383 -0
- package/dictionary/money.toon +51 -0
- package/dictionary/ports.json +392 -0
- package/dictionary/ports.toon +52 -0
- package/dictionary/postgres.json +473 -0
- package/dictionary/postgres.toon +61 -0
- package/dictionary/publishing.json +131 -0
- package/dictionary/publishing.toon +23 -0
- package/dictionary/queue.json +365 -0
- package/dictionary/queue.toon +49 -0
- package/dictionary/reference/api.mdx +843 -0
- package/dictionary/reference/uk/api.mdx +844 -0
- package/dictionary/storage.json +194 -0
- package/dictionary/storage.toon +30 -0
- package/dictionary/testing.json +707 -0
- package/dictionary/testing.toon +87 -0
- package/dictionary/time.json +311 -0
- package/dictionary/time.toon +43 -0
- package/dictionary/vitest.json +356 -0
- package/dictionary/vitest.toon +48 -0
- package/dictionary/workflow.json +185 -0
- package/dictionary/workflow.toon +29 -0
- package/dist/bin/command.d.ts +45 -0
- package/dist/bin/command.js +113 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +11 -0
- package/dist/core/bigint-json.d.ts +6 -0
- package/dist/core/bigint-json.js +6 -0
- package/dist/core/closed-set.d.ts +14 -0
- package/dist/core/closed-set.js +8 -0
- package/dist/core/domain-error.d.ts +8 -0
- package/dist/core/domain-error.js +11 -0
- package/dist/core/environment.d.ts +124 -0
- package/dist/core/environment.js +115 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +13 -0
- package/dist/core/ulid.d.ts +27 -0
- package/dist/core/ulid.js +27 -0
- package/dist/graphql/builder.d.ts +36 -0
- package/dist/graphql/builder.js +35 -0
- package/dist/graphql/context.d.ts +36 -0
- package/dist/graphql/context.js +69 -0
- package/dist/graphql/failure-classification.d.ts +11 -0
- package/dist/graphql/failure-classification.js +1 -0
- package/dist/graphql/failures.d.ts +62 -0
- package/dist/graphql/failures.js +30 -0
- package/dist/graphql/graphiql.d.ts +45 -0
- package/dist/graphql/graphiql.js +56 -0
- package/dist/graphql/index.d.ts +15 -0
- package/dist/graphql/index.js +14 -0
- package/dist/graphql/layer.d.ts +15 -0
- package/dist/graphql/layer.js +23 -0
- package/dist/graphql/request-handler.d.ts +26 -0
- package/dist/graphql/request-handler.js +33 -0
- package/dist/graphql/server.d.ts +19 -0
- package/dist/graphql/server.js +16 -0
- package/dist/graphql/settings.d.ts +55 -0
- package/dist/graphql/settings.js +82 -0
- package/dist/http/health.d.ts +70 -0
- package/dist/http/health.js +60 -0
- package/dist/http/index.d.ts +5 -0
- package/dist/http/index.js +5 -0
- package/dist/identifiers/client-code.d.ts +37 -0
- package/dist/identifiers/client-code.js +41 -0
- package/dist/identifiers/creditor-reference.d.ts +38 -0
- package/dist/identifiers/creditor-reference.js +32 -0
- package/dist/identifiers/damm.d.ts +22 -0
- package/dist/identifiers/damm.js +50 -0
- package/dist/identifiers/document-number.d.ts +68 -0
- package/dist/identifiers/document-number.js +73 -0
- package/dist/identifiers/document-numbers.d.ts +27 -0
- package/dist/identifiers/document-numbers.js +16 -0
- package/dist/identifiers/failures.d.ts +46 -0
- package/dist/identifiers/failures.js +48 -0
- package/dist/identifiers/index.d.ts +14 -0
- package/dist/identifiers/index.js +13 -0
- package/dist/identifiers/next-document-number.d.ts +26 -0
- package/dist/identifiers/next-document-number.js +31 -0
- package/dist/identifiers/ulid.d.ts +28 -0
- package/dist/identifiers/ulid.js +71 -0
- package/dist/integration/external-http-client.d.ts +22 -0
- package/dist/integration/external-http-client.js +74 -0
- package/dist/integration/index.d.ts +12 -0
- package/dist/integration/index.js +12 -0
- package/dist/integration/layer.d.ts +19 -0
- package/dist/integration/layer.js +30 -0
- package/dist/integration/retry.d.ts +65 -0
- package/dist/integration/retry.js +54 -0
- package/dist/integration/settings.d.ts +73 -0
- package/dist/integration/settings.js +92 -0
- package/dist/money/amount.d.ts +60 -0
- package/dist/money/amount.js +67 -0
- package/dist/money/arithmetic.d.ts +60 -0
- package/dist/money/arithmetic.js +73 -0
- package/dist/money/currency.d.ts +25 -0
- package/dist/money/currency.js +24 -0
- package/dist/money/decimal.d.ts +29 -0
- package/dist/money/decimal.js +39 -0
- package/dist/money/exchange-rate.d.ts +32 -0
- package/dist/money/exchange-rate.js +28 -0
- package/dist/money/failures.d.ts +86 -0
- package/dist/money/failures.js +97 -0
- package/dist/money/index.d.ts +15 -0
- package/dist/money/index.js +15 -0
- package/dist/money/percent.d.ts +35 -0
- package/dist/money/percent.js +50 -0
- package/dist/money/schema.d.ts +25 -0
- package/dist/money/schema.js +41 -0
- package/dist/ports/blob-store.d.ts +42 -0
- package/dist/ports/blob-store.js +13 -0
- package/dist/ports/document-number-counter.d.ts +36 -0
- package/dist/ports/document-number-counter.js +12 -0
- package/dist/ports/event-publisher.d.ts +29 -0
- package/dist/ports/event-publisher.js +13 -0
- package/dist/ports/external-http-client.d.ts +60 -0
- package/dist/ports/external-http-client.js +25 -0
- package/dist/ports/index.d.ts +17 -0
- package/dist/ports/index.js +17 -0
- package/dist/ports/integration-event-outbox.d.ts +44 -0
- package/dist/ports/integration-event-outbox.js +13 -0
- package/dist/ports/port-unavailable.d.ts +21 -0
- package/dist/ports/port-unavailable.js +30 -0
- package/dist/ports/processed-event-registry.d.ts +57 -0
- package/dist/ports/processed-event-registry.js +24 -0
- package/dist/ports/queue-message-producer.d.ts +26 -0
- package/dist/ports/queue-message-producer.js +12 -0
- package/dist/ports/queue-message-source.d.ts +39 -0
- package/dist/ports/queue-message-source.js +13 -0
- package/dist/ports/readiness-probe.d.ts +27 -0
- package/dist/ports/readiness-probe.js +13 -0
- package/dist/ports/transaction-boundary.d.ts +28 -0
- package/dist/ports/transaction-boundary.js +13 -0
- package/dist/postgres/client.d.ts +51 -0
- package/dist/postgres/client.js +81 -0
- package/dist/postgres/document-number-counter.d.ts +14 -0
- package/dist/postgres/document-number-counter.js +27 -0
- package/dist/postgres/failures.d.ts +28 -0
- package/dist/postgres/failures.js +18 -0
- package/dist/postgres/index.d.ts +18 -0
- package/dist/postgres/index.js +18 -0
- package/dist/postgres/integration-event-outbox.d.ts +15 -0
- package/dist/postgres/integration-event-outbox.js +60 -0
- package/dist/postgres/jsonb-value.d.ts +10 -0
- package/dist/postgres/jsonb-value.js +9 -0
- package/dist/postgres/layer.d.ts +57 -0
- package/dist/postgres/layer.js +72 -0
- package/dist/postgres/migrations/advisory-lock.d.ts +47 -0
- package/dist/postgres/migrations/advisory-lock.js +59 -0
- package/dist/postgres/migrations/runner.d.ts +56 -0
- package/dist/postgres/migrations/runner.js +69 -0
- package/dist/postgres/migrations/sql-file-loader.d.ts +59 -0
- package/dist/postgres/migrations/sql-file-loader.js +99 -0
- package/dist/postgres/processed-event-registry.d.ts +13 -0
- package/dist/postgres/processed-event-registry.js +44 -0
- package/dist/postgres/readiness-probe.d.ts +27 -0
- package/dist/postgres/readiness-probe.js +33 -0
- package/dist/postgres/settings.d.ts +134 -0
- package/dist/postgres/settings.js +210 -0
- package/dist/postgres/sql/0001_processed_event.sql +4 -0
- package/dist/postgres/sql/0002_integration_event_outbox.sql +13 -0
- package/dist/postgres/sql/0003_document_number_counter.sql +6 -0
- package/dist/postgres/sql-failure.d.ts +19 -0
- package/dist/postgres/sql-failure.js +22 -0
- package/dist/postgres/store-failure.d.ts +17 -0
- package/dist/postgres/store-failure.js +16 -0
- package/dist/postgres/transaction-boundary.d.ts +13 -0
- package/dist/postgres/transaction-boundary.js +21 -0
- package/dist/publishing/event-publisher.sns.d.ts +66 -0
- package/dist/publishing/event-publisher.sns.js +75 -0
- package/dist/publishing/event-publisher.stdout.d.ts +15 -0
- package/dist/publishing/event-publisher.stdout.js +24 -0
- package/dist/publishing/index.d.ts +10 -0
- package/dist/publishing/index.js +10 -0
- package/dist/publishing/layer.d.ts +25 -0
- package/dist/publishing/layer.js +39 -0
- package/dist/publishing/settings.d.ts +60 -0
- package/dist/publishing/settings.js +85 -0
- package/dist/queue/consumer.d.ts +91 -0
- package/dist/queue/consumer.js +92 -0
- package/dist/queue/envelope.d.ts +39 -0
- package/dist/queue/envelope.js +39 -0
- package/dist/queue/failures.d.ts +33 -0
- package/dist/queue/failures.js +15 -0
- package/dist/queue/index.d.ts +14 -0
- package/dist/queue/index.js +14 -0
- package/dist/queue/layer.d.ts +33 -0
- package/dist/queue/layer.js +58 -0
- package/dist/queue/message-codec.d.ts +43 -0
- package/dist/queue/message-codec.js +33 -0
- package/dist/queue/queue-message-producer.d.ts +61 -0
- package/dist/queue/queue-message-producer.js +52 -0
- package/dist/queue/queue-message-source.d.ts +93 -0
- package/dist/queue/queue-message-source.js +86 -0
- package/dist/queue/readiness.d.ts +78 -0
- package/dist/queue/readiness.js +86 -0
- package/dist/queue/settings.d.ts +106 -0
- package/dist/queue/settings.js +160 -0
- package/dist/storage/blob-store.d.ts +84 -0
- package/dist/storage/blob-store.js +109 -0
- package/dist/storage/index.d.ts +9 -0
- package/dist/storage/index.js +9 -0
- package/dist/storage/layer.d.ts +17 -0
- package/dist/storage/layer.js +30 -0
- package/dist/storage/settings.d.ts +48 -0
- package/dist/storage/settings.js +65 -0
- package/dist/testing/blob-store.d.ts +27 -0
- package/dist/testing/blob-store.js +45 -0
- package/dist/testing/clock.d.ts +29 -0
- package/dist/testing/clock.js +41 -0
- package/dist/testing/dna-perturbator/attention.d.ts +55 -0
- package/dist/testing/dna-perturbator/attention.js +49 -0
- package/dist/testing/dna-perturbator/chains.d.ts +31 -0
- package/dist/testing/dna-perturbator/chains.js +53 -0
- package/dist/testing/dna-perturbator/report.d.ts +12 -0
- package/dist/testing/dna-perturbator/report.js +22 -0
- package/dist/testing/dna-perturbator/verdict.d.ts +26 -0
- package/dist/testing/dna-perturbator/verdict.js +28 -0
- package/dist/testing/dna-perturbator.d.ts +48 -0
- package/dist/testing/dna-perturbator.js +145 -0
- package/dist/testing/document-number-counter.d.ts +29 -0
- package/dist/testing/document-number-counter.js +39 -0
- package/dist/testing/event-publisher.d.ts +28 -0
- package/dist/testing/event-publisher.js +33 -0
- package/dist/testing/external-http-client.d.ts +29 -0
- package/dist/testing/external-http-client.js +49 -0
- package/dist/testing/index.d.ts +24 -0
- package/dist/testing/index.js +24 -0
- package/dist/testing/integration-event-outbox.d.ts +37 -0
- package/dist/testing/integration-event-outbox.js +67 -0
- package/dist/testing/mutation-settings.d.ts +71 -0
- package/dist/testing/mutation-settings.js +57 -0
- package/dist/testing/postgres-rows.d.ts +12 -0
- package/dist/testing/postgres-rows.js +19 -0
- package/dist/testing/processed-event-registry.d.ts +25 -0
- package/dist/testing/processed-event-registry.js +39 -0
- package/dist/testing/queue-message-producer.d.ts +27 -0
- package/dist/testing/queue-message-producer.js +41 -0
- package/dist/testing/queue-message-source.d.ts +54 -0
- package/dist/testing/queue-message-source.js +71 -0
- package/dist/testing/quiet-reporter.d.ts +86 -0
- package/dist/testing/quiet-reporter.js +81 -0
- package/dist/testing/readiness-probe.d.ts +27 -0
- package/dist/testing/readiness-probe.js +33 -0
- package/dist/testing/scenario.d.ts +94 -0
- package/dist/testing/scenario.js +81 -0
- package/dist/testing/transaction-boundary.d.ts +30 -0
- package/dist/testing/transaction-boundary.js +36 -0
- package/dist/testing/typed-failure.d.ts +8 -0
- package/dist/testing/typed-failure.js +15 -0
- package/dist/time/calendar-day.d.ts +83 -0
- package/dist/time/calendar-day.js +112 -0
- package/dist/time/failures.d.ts +51 -0
- package/dist/time/failures.js +56 -0
- package/dist/time/index.d.ts +11 -0
- package/dist/time/index.js +11 -0
- package/dist/time/instant.d.ts +64 -0
- package/dist/time/instant.js +67 -0
- package/dist/time/period.d.ts +45 -0
- package/dist/time/period.js +55 -0
- package/dist/vitest/container-reuse.d.ts +14 -0
- package/dist/vitest/container-reuse.js +15 -0
- package/dist/vitest/dotenv.d.ts +16 -0
- package/dist/vitest/dotenv.js +48 -0
- package/dist/vitest/effect-single-instance.d.ts +36 -0
- package/dist/vitest/effect-single-instance.js +125 -0
- package/dist/vitest/fast-check-single-instance.d.ts +11 -0
- package/dist/vitest/fast-check-single-instance.js +26 -0
- package/dist/vitest/index.d.ts +31 -0
- package/dist/vitest/index.js +30 -0
- package/dist/vitest/localstack-container.d.ts +32 -0
- package/dist/vitest/localstack-container.js +55 -0
- package/dist/vitest/port-contract-tester.d.ts +8 -0
- package/dist/vitest/port-contract-tester.js +1 -0
- package/dist/vitest/port-contracts/blob-store.contract.d.ts +22 -0
- package/dist/vitest/port-contracts/blob-store.contract.js +108 -0
- package/dist/vitest/port-contracts/document-number-counter.contract.d.ts +18 -0
- package/dist/vitest/port-contracts/document-number-counter.contract.js +90 -0
- package/dist/vitest/port-contracts/event-publisher.contract.d.ts +35 -0
- package/dist/vitest/port-contracts/event-publisher.contract.js +67 -0
- package/dist/vitest/port-contracts/external-http-client.contract.d.ts +35 -0
- package/dist/vitest/port-contracts/external-http-client.contract.js +52 -0
- package/dist/vitest/port-contracts/integration-event-outbox.contract.d.ts +23 -0
- package/dist/vitest/port-contracts/integration-event-outbox.contract.js +75 -0
- package/dist/vitest/port-contracts/processed-event-registry.contract.d.ts +18 -0
- package/dist/vitest/port-contracts/processed-event-registry.contract.js +84 -0
- package/dist/vitest/port-contracts/queue-message-producer.contract.d.ts +26 -0
- package/dist/vitest/port-contracts/queue-message-producer.contract.js +48 -0
- package/dist/vitest/port-contracts/queue-message-source.contract.d.ts +29 -0
- package/dist/vitest/port-contracts/queue-message-source.contract.js +71 -0
- package/dist/vitest/port-contracts/readiness-probe.contract.d.ts +18 -0
- package/dist/vitest/port-contracts/readiness-probe.contract.js +41 -0
- package/dist/vitest/port-contracts/transaction-boundary.contract.d.ts +21 -0
- package/dist/vitest/port-contracts/transaction-boundary.contract.js +64 -0
- package/dist/vitest/postgres-container.d.ts +57 -0
- package/dist/vitest/postgres-container.js +118 -0
- package/dist/vitest/reader-port-contract.d.ts +41 -0
- package/dist/vitest/reader-port-contract.js +43 -0
- package/dist/vitest/repo-source-files.d.ts +37 -0
- package/dist/vitest/repo-source-files.js +61 -0
- package/dist/vitest/reset-tables.d.ts +18 -0
- package/dist/vitest/reset-tables.js +30 -0
- package/dist/vitest/settings-contract.d.ts +38 -0
- package/dist/vitest/settings-contract.js +77 -0
- package/dist/vitest/stub-server.d.ts +43 -0
- package/dist/vitest/stub-server.js +60 -0
- package/dist/vitest/term-registry.json +730 -0
- package/dist/vitest/testclock-fork-isolated-files.d.ts +13 -0
- package/dist/vitest/testclock-fork-isolated-files.js +18 -0
- package/dist/vitest/testclock-fork-isolation.d.ts +77 -0
- package/dist/vitest/testclock-fork-isolation.js +188 -0
- package/dist/workflow/claim-once.d.ts +16 -0
- package/dist/workflow/claim-once.js +9 -0
- package/dist/workflow/failures.d.ts +47 -0
- package/dist/workflow/failures.js +22 -0
- package/dist/workflow/index.d.ts +12 -0
- package/dist/workflow/index.js +12 -0
- package/dist/workflow/on-claim.d.ts +17 -0
- package/dist/workflow/on-claim.js +8 -0
- package/dist/workflow/publish-pending-events.d.ts +31 -0
- package/dist/workflow/publish-pending-events.js +31 -0
- package/dist/workflow/transactionally.d.ts +8 -0
- package/dist/workflow/transactionally.js +8 -0
- package/dist/workflow/versioned-payload.d.ts +35 -0
- package/dist/workflow/versioned-payload.js +43 -0
- package/package.json +309 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { type EnvironmentEntry, type Refusal } from '@satorio/machinery/core';
|
|
2
|
+
import { Config, type ConfigError, ConfigProvider, Either, Option, type Redacted } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* Every way a connection can be encrypted and verified, from no encryption at
|
|
5
|
+
* all to a fully verified chain.
|
|
6
|
+
*/
|
|
7
|
+
export declare const POSTGRES_SSL_MODES: readonly ["disable", "allow", "prefer", "require", "verify-ca", "verify-full"];
|
|
8
|
+
/**
|
|
9
|
+
* One of the ways a connection can be encrypted and verified.
|
|
10
|
+
*/
|
|
11
|
+
export type PostgresSslMode = (typeof POSTGRES_SSL_MODES)[number];
|
|
12
|
+
/**
|
|
13
|
+
* The port a Postgres server listens on when nobody says otherwise.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DEFAULT_POSTGRES_PORT = 5432;
|
|
16
|
+
/**
|
|
17
|
+
* The schema every statement runs against when nobody names one.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DEFAULT_POSTGRES_SCHEMA = "app";
|
|
20
|
+
/**
|
|
21
|
+
* How a connection is encrypted when nobody says otherwise: the chain is
|
|
22
|
+
* verified, so the safe choice is the one you get by saying nothing.
|
|
23
|
+
*/
|
|
24
|
+
export declare const DEFAULT_POSTGRES_SSL_MODE: PostgresSslMode;
|
|
25
|
+
/**
|
|
26
|
+
* The name this service shows under in the database's own list of connections
|
|
27
|
+
* when nobody names it.
|
|
28
|
+
*/
|
|
29
|
+
export declare const DEFAULT_APPLICATION_NAME = "machinery";
|
|
30
|
+
/**
|
|
31
|
+
* How a service proves who it is when nothing says otherwise. A password
|
|
32
|
+
* is what a newcomer already has on a local database; production names
|
|
33
|
+
* the stronger choice out loud with `AUTH_MODE=iam`.
|
|
34
|
+
*/
|
|
35
|
+
export declare const DEFAULT_AUTH_MODE: 'iam' | 'password';
|
|
36
|
+
/**
|
|
37
|
+
* The two ways to prove to the database who you are: a short-lived IAM
|
|
38
|
+
* token or a password. The list is closed, so a third way never appears
|
|
39
|
+
* silently. The IAM region is optional, the way it is everywhere else in
|
|
40
|
+
* this package — the AWS SDK finds its own when nobody names one.
|
|
41
|
+
*/
|
|
42
|
+
export type PostgresAuthentication = {
|
|
43
|
+
readonly _tag: 'AwsIamAuthentication';
|
|
44
|
+
readonly region: Option.Option<string>;
|
|
45
|
+
} | {
|
|
46
|
+
readonly _tag: 'PasswordAuthentication';
|
|
47
|
+
readonly password: Redacted.Redacted;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Everything one connection needs, read in one go: where the server is, who
|
|
51
|
+
* connects, and how the connection is proven and encrypted.
|
|
52
|
+
*/
|
|
53
|
+
export interface PostgresConnectionSettings {
|
|
54
|
+
readonly host: string;
|
|
55
|
+
readonly port: number;
|
|
56
|
+
readonly user: string;
|
|
57
|
+
readonly database: string;
|
|
58
|
+
readonly schema: string;
|
|
59
|
+
readonly applicationName: string;
|
|
60
|
+
readonly sslMode: PostgresSslMode;
|
|
61
|
+
readonly sslRootCertificatePath: Option.Option<string>;
|
|
62
|
+
readonly authentication: PostgresAuthentication;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The three parts a reader meets when the database address is missing.
|
|
66
|
+
*
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
export declare const hostRefusal: Refusal;
|
|
70
|
+
/**
|
|
71
|
+
* The three parts a reader meets when nobody says who connects.
|
|
72
|
+
*
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
export declare const userRefusal: Refusal;
|
|
76
|
+
/**
|
|
77
|
+
* The three parts a reader meets when nobody says which database.
|
|
78
|
+
*
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
export declare const databaseRefusal: Refusal;
|
|
82
|
+
/**
|
|
83
|
+
* The three parts a reader meets when the password is missing. It names
|
|
84
|
+
* the default that put them on this path and both ways off it.
|
|
85
|
+
*
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
export declare const passwordRefusal: Refusal;
|
|
89
|
+
/**
|
|
90
|
+
* Every variable this adapter reads, and the one state each of them
|
|
91
|
+
* stands in.
|
|
92
|
+
*/
|
|
93
|
+
export declare const ENVIRONMENT: readonly EnvironmentEntry[];
|
|
94
|
+
interface AuthenticationCandidates {
|
|
95
|
+
readonly mode: 'iam' | 'password';
|
|
96
|
+
readonly password: Option.Option<Redacted.Redacted>;
|
|
97
|
+
readonly region: Option.Option<string>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Reads both ways of proving who this service is, then keeps the one
|
|
101
|
+
* `AUTH_MODE` names. Both are read before either is chosen, so every
|
|
102
|
+
* refusal of this settings block reaches the reader in one answer
|
|
103
|
+
* instead of one at a time.
|
|
104
|
+
*
|
|
105
|
+
* @internal
|
|
106
|
+
*/
|
|
107
|
+
export declare const authenticationFrom: ({ mode, password, region, }: AuthenticationCandidates) => Either.Either<PostgresAuthentication, ConfigError.ConfigError>;
|
|
108
|
+
/**
|
|
109
|
+
* Every variable of `ENVIRONMENT`, read in one go. A missing required
|
|
110
|
+
* variable refuses in three parts, and an environment missing several of
|
|
111
|
+
* them refuses about all of them at once, so an operator fixes one .env
|
|
112
|
+
* rather than restarting after every line.
|
|
113
|
+
*/
|
|
114
|
+
export declare const postgresConnectionSettings: Config.Config<PostgresConnectionSettings>;
|
|
115
|
+
/**
|
|
116
|
+
* Builds a config provider that reads from `primary`, falling back to one
|
|
117
|
+
* fixed value when `primary` has no such key. Kept separate from
|
|
118
|
+
* `schemaDefaultingTo` so a test can check "environment wins, default is
|
|
119
|
+
* only a fallback" against an arbitrary `primary`, without touching the
|
|
120
|
+
* real `process.env`.
|
|
121
|
+
*/
|
|
122
|
+
export declare const configProviderPreferring: (primary: ConfigProvider.ConfigProvider, fallbackKey: string, fallbackValue: string) => ConfigProvider.ConfigProvider;
|
|
123
|
+
/**
|
|
124
|
+
* Default schema for one service when `PGSCHEMA` isn't set. The
|
|
125
|
+
* environment always wins; this default only guards a local Postgres that
|
|
126
|
+
* several services share in turn. The migrations runner tracks the last
|
|
127
|
+
* applied migration per schema, not per service, so a shared schema
|
|
128
|
+
* without separate numbers would silently treat one service's
|
|
129
|
+
* migrations as already applied. A deployed service always has its own
|
|
130
|
+
* database or its own schema in the environment, so the mismatch never
|
|
131
|
+
* arises there.
|
|
132
|
+
*/
|
|
133
|
+
export declare const schemaDefaultingTo: (schema: string) => ConfigProvider.ConfigProvider;
|
|
134
|
+
export {};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { fallsBackTo, handedOn, optionalSecret, optionalText, refusalFor, refuses, requiredText, } from '@satorio/machinery/core';
|
|
2
|
+
import { Config, ConfigProvider, Either, Option, } from 'effect';
|
|
3
|
+
const HOST = 'PGHOST';
|
|
4
|
+
const PORT = 'PGPORT';
|
|
5
|
+
const USER = 'PGUSER';
|
|
6
|
+
const DATABASE = 'PGDATABASE';
|
|
7
|
+
const SCHEMA = 'PGSCHEMA';
|
|
8
|
+
const APPLICATION_NAME = 'PGAPPNAME';
|
|
9
|
+
const SSL_MODE = 'PGSSLMODE';
|
|
10
|
+
const SSL_ROOT_CERTIFICATE = 'PGSSLROOTCERT';
|
|
11
|
+
const AUTHENTICATION_MODE = 'AUTH_MODE';
|
|
12
|
+
const PASSWORD = 'PGPASSWORD';
|
|
13
|
+
const REGION = 'AWS_REGION';
|
|
14
|
+
/**
|
|
15
|
+
* Every way a connection can be encrypted and verified, from no encryption at
|
|
16
|
+
* all to a fully verified chain.
|
|
17
|
+
*/
|
|
18
|
+
export const POSTGRES_SSL_MODES = [
|
|
19
|
+
'disable',
|
|
20
|
+
'allow',
|
|
21
|
+
'prefer',
|
|
22
|
+
'require',
|
|
23
|
+
'verify-ca',
|
|
24
|
+
'verify-full',
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* The port a Postgres server listens on when nobody says otherwise.
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_POSTGRES_PORT = 5432;
|
|
30
|
+
/**
|
|
31
|
+
* The schema every statement runs against when nobody names one.
|
|
32
|
+
*/
|
|
33
|
+
export const DEFAULT_POSTGRES_SCHEMA = 'app';
|
|
34
|
+
/**
|
|
35
|
+
* How a connection is encrypted when nobody says otherwise: the chain is
|
|
36
|
+
* verified, so the safe choice is the one you get by saying nothing.
|
|
37
|
+
*/
|
|
38
|
+
export const DEFAULT_POSTGRES_SSL_MODE = 'verify-full';
|
|
39
|
+
/**
|
|
40
|
+
* The name this service shows under in the database's own list of connections
|
|
41
|
+
* when nobody names it.
|
|
42
|
+
*/
|
|
43
|
+
export const DEFAULT_APPLICATION_NAME = 'machinery';
|
|
44
|
+
/**
|
|
45
|
+
* How a service proves who it is when nothing says otherwise. A password
|
|
46
|
+
* is what a newcomer already has on a local database; production names
|
|
47
|
+
* the stronger choice out loud with `AUTH_MODE=iam`.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_AUTH_MODE = 'password';
|
|
50
|
+
/**
|
|
51
|
+
* The three parts a reader meets when the database address is missing.
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
export const hostRefusal = {
|
|
56
|
+
what: `${HOST} is not set, so this service does not know which database to open.`,
|
|
57
|
+
why: 'A connection pool needs an address before it can open anything, and there is no address a service could sensibly guess.',
|
|
58
|
+
whatToDo: `Set ${HOST} to the database host from your .env or your deployment, then start the service again.`,
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The three parts a reader meets when nobody says who connects.
|
|
62
|
+
*
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
export const userRefusal = {
|
|
66
|
+
what: `${USER} is not set, so this service has no database user to connect as.`,
|
|
67
|
+
why: 'Postgres grants rights per role, so the connection cannot be opened under a role nobody named.',
|
|
68
|
+
whatToDo: `Set ${USER} to the role this service owns, then start the service again.`,
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* The three parts a reader meets when nobody says which database.
|
|
72
|
+
*
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
export const databaseRefusal = {
|
|
76
|
+
what: `${DATABASE} is not set, so this service does not know which database to connect to.`,
|
|
77
|
+
why: 'One Postgres server holds many databases, and the wrong one would answer every query with tables this service never wrote.',
|
|
78
|
+
whatToDo: `Set ${DATABASE} to the database name this service owns, then start the service again.`,
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* The three parts a reader meets when the password is missing. It names
|
|
82
|
+
* the default that put them on this path and both ways off it.
|
|
83
|
+
*
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
export const passwordRefusal = {
|
|
87
|
+
what: `${PASSWORD} is not set, and ${AUTHENTICATION_MODE} defaulted to password.`,
|
|
88
|
+
why: 'The connection has no way left to prove who it is to the database.',
|
|
89
|
+
whatToDo: `Set ${PASSWORD} to the database password, or set ${AUTHENTICATION_MODE}=iam to sign a short-lived AWS IAM token instead.`,
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Every variable this adapter reads, and the one state each of them
|
|
93
|
+
* stands in.
|
|
94
|
+
*/
|
|
95
|
+
export const ENVIRONMENT = [
|
|
96
|
+
{
|
|
97
|
+
name: HOST,
|
|
98
|
+
what: 'The host this service opens its connection pool against.',
|
|
99
|
+
state: refuses(hostRefusal),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: PORT,
|
|
103
|
+
what: 'The TCP port the database listens on.',
|
|
104
|
+
state: fallsBackTo(String(DEFAULT_POSTGRES_PORT)),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: USER,
|
|
108
|
+
what: 'The Postgres role this service connects as.',
|
|
109
|
+
state: refuses(userRefusal),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: DATABASE,
|
|
113
|
+
what: 'The database this service reads and writes.',
|
|
114
|
+
state: refuses(databaseRefusal),
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: SCHEMA,
|
|
118
|
+
what: "The schema every statement runs against, set as the connection's search_path.",
|
|
119
|
+
state: fallsBackTo(DEFAULT_POSTGRES_SCHEMA),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: APPLICATION_NAME,
|
|
123
|
+
what: 'The name this service shows under in pg_stat_activity.',
|
|
124
|
+
state: fallsBackTo(DEFAULT_APPLICATION_NAME),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: SSL_MODE,
|
|
128
|
+
what: `How the connection is encrypted and verified, one of ${POSTGRES_SSL_MODES.join(', ')}.`,
|
|
129
|
+
state: fallsBackTo(DEFAULT_POSTGRES_SSL_MODE),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: SSL_ROOT_CERTIFICATE,
|
|
133
|
+
what: 'The path to a certificate authority file the database chain is verified against.',
|
|
134
|
+
state: handedOn("the driver verifies the chain against the machine's own trust store."),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: AUTHENTICATION_MODE,
|
|
138
|
+
what: 'Which of the two ways this service proves who it is, iam or password.',
|
|
139
|
+
state: fallsBackTo(DEFAULT_AUTH_MODE),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: PASSWORD,
|
|
143
|
+
what: 'The database password, read under the default password mode.',
|
|
144
|
+
state: refuses(passwordRefusal),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: REGION,
|
|
148
|
+
what: 'The region the IAM token signer signs against, read under AUTH_MODE=iam.',
|
|
149
|
+
state: handedOn('the AWS SDK finds its own region from the machine it runs on.'),
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
const authenticationMode = Config.literal('iam', 'password')(AUTHENTICATION_MODE).pipe(Config.withDefault(DEFAULT_AUTH_MODE));
|
|
153
|
+
/**
|
|
154
|
+
* Reads both ways of proving who this service is, then keeps the one
|
|
155
|
+
* `AUTH_MODE` names. Both are read before either is chosen, so every
|
|
156
|
+
* refusal of this settings block reaches the reader in one answer
|
|
157
|
+
* instead of one at a time.
|
|
158
|
+
*
|
|
159
|
+
* @internal
|
|
160
|
+
*/
|
|
161
|
+
export const authenticationFrom = ({ mode, password, region, }) => {
|
|
162
|
+
if (mode === 'iam') {
|
|
163
|
+
return Either.right({ _tag: 'AwsIamAuthentication', region });
|
|
164
|
+
}
|
|
165
|
+
return Option.match(password, {
|
|
166
|
+
onNone: () => Either.left(refusalFor(PASSWORD, passwordRefusal)),
|
|
167
|
+
onSome: (secret) => Either.right({ _tag: 'PasswordAuthentication', password: secret }),
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
const authentication = Config.all({
|
|
171
|
+
mode: authenticationMode,
|
|
172
|
+
password: optionalSecret(PASSWORD),
|
|
173
|
+
region: optionalText(REGION),
|
|
174
|
+
}).pipe(Config.mapOrFail(authenticationFrom));
|
|
175
|
+
/**
|
|
176
|
+
* Every variable of `ENVIRONMENT`, read in one go. A missing required
|
|
177
|
+
* variable refuses in three parts, and an environment missing several of
|
|
178
|
+
* them refuses about all of them at once, so an operator fixes one .env
|
|
179
|
+
* rather than restarting after every line.
|
|
180
|
+
*/
|
|
181
|
+
export const postgresConnectionSettings = Config.all({
|
|
182
|
+
host: requiredText(HOST, hostRefusal),
|
|
183
|
+
port: Config.integer(PORT).pipe(Config.withDefault(DEFAULT_POSTGRES_PORT)),
|
|
184
|
+
user: requiredText(USER, userRefusal),
|
|
185
|
+
database: requiredText(DATABASE, databaseRefusal),
|
|
186
|
+
schema: Config.string(SCHEMA).pipe(Config.withDefault(DEFAULT_POSTGRES_SCHEMA)),
|
|
187
|
+
applicationName: Config.string(APPLICATION_NAME).pipe(Config.withDefault(DEFAULT_APPLICATION_NAME)),
|
|
188
|
+
sslMode: Config.literal(...POSTGRES_SSL_MODES)(SSL_MODE).pipe(Config.withDefault(DEFAULT_POSTGRES_SSL_MODE)),
|
|
189
|
+
sslRootCertificatePath: optionalText(SSL_ROOT_CERTIFICATE),
|
|
190
|
+
authentication,
|
|
191
|
+
});
|
|
192
|
+
/**
|
|
193
|
+
* Builds a config provider that reads from `primary`, falling back to one
|
|
194
|
+
* fixed value when `primary` has no such key. Kept separate from
|
|
195
|
+
* `schemaDefaultingTo` so a test can check "environment wins, default is
|
|
196
|
+
* only a fallback" against an arbitrary `primary`, without touching the
|
|
197
|
+
* real `process.env`.
|
|
198
|
+
*/
|
|
199
|
+
export const configProviderPreferring = (primary, fallbackKey, fallbackValue) => primary.pipe(ConfigProvider.orElse(() => ConfigProvider.fromMap(new Map([[fallbackKey, fallbackValue]]))));
|
|
200
|
+
/**
|
|
201
|
+
* Default schema for one service when `PGSCHEMA` isn't set. The
|
|
202
|
+
* environment always wins; this default only guards a local Postgres that
|
|
203
|
+
* several services share in turn. The migrations runner tracks the last
|
|
204
|
+
* applied migration per schema, not per service, so a shared schema
|
|
205
|
+
* without separate numbers would silently treat one service's
|
|
206
|
+
* migrations as already applied. A deployed service always has its own
|
|
207
|
+
* database or its own schema in the environment, so the mismatch never
|
|
208
|
+
* arises there.
|
|
209
|
+
*/
|
|
210
|
+
export const schemaDefaultingTo = (schema) => configProviderPreferring(ConfigProvider.fromEnv(), SCHEMA, schema);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS integration_event_outbox (
|
|
2
|
+
sequence_number bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
3
|
+
message_id text NOT NULL UNIQUE,
|
|
4
|
+
event_type text NOT NULL,
|
|
5
|
+
schema_version integer NOT NULL,
|
|
6
|
+
occurred_at timestamptz NOT NULL,
|
|
7
|
+
payload jsonb NOT NULL,
|
|
8
|
+
dispatched_at timestamptz
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
CREATE INDEX IF NOT EXISTS integration_event_outbox_pending
|
|
12
|
+
ON integration_event_outbox (sequence_number)
|
|
13
|
+
WHERE dispatched_at IS NULL;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SqlError } from '@effect/sql/SqlError';
|
|
2
|
+
import { Option } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* The SQLSTATE Postgres answers a duplicate key with.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare const UNIQUE_VIOLATION_SQL_STATE = "23505";
|
|
9
|
+
/**
|
|
10
|
+
* The SQLSTATE a driver failure carries, when it carries one.
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export declare const sqlStateOf: (error: SqlError) => Option.Option<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Whether this driver failure is a duplicate key rather than anything
|
|
17
|
+
* else the database could refuse.
|
|
18
|
+
*/
|
|
19
|
+
export declare const isUniqueViolation: (error: SqlError) => boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Option } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* The SQLSTATE Postgres answers a duplicate key with.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export const UNIQUE_VIOLATION_SQL_STATE = '23505';
|
|
8
|
+
const carriesStringCode = (cause) => typeof cause === 'object' &&
|
|
9
|
+
cause !== null &&
|
|
10
|
+
'code' in cause &&
|
|
11
|
+
typeof cause.code === 'string';
|
|
12
|
+
/**
|
|
13
|
+
* The SQLSTATE a driver failure carries, when it carries one.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export const sqlStateOf = (error) => carriesStringCode(error.cause) ? Option.some(error.cause.code) : Option.none();
|
|
18
|
+
/**
|
|
19
|
+
* Whether this driver failure is a duplicate key rather than anything
|
|
20
|
+
* else the database could refuse.
|
|
21
|
+
*/
|
|
22
|
+
export const isUniqueViolation = (error) => Option.contains(sqlStateOf(error), UNIQUE_VIOLATION_SQL_STATE);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SqlError } from '@effect/sql/SqlError';
|
|
2
|
+
import { Effect, Option } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* Splits a write path at the database boundary: a unique violation
|
|
5
|
+
* becomes a domain conflict, any other SqlError becomes "store
|
|
6
|
+
* unavailable". Pass a `unique` branch that builds the conflict failure
|
|
7
|
+
* and an `unavailable` branch, `portUnavailable` bound to `operation`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const onUniqueViolation: <Conflict, Unavailable>(operation: string, branches: {
|
|
10
|
+
readonly unique: () => Conflict;
|
|
11
|
+
readonly unavailable: (operation: string) => (cause: unknown) => Unavailable;
|
|
12
|
+
}) => <A, R>(effect: Effect.Effect<A, SqlError, R>) => Effect.Effect<A, Conflict | Unavailable, R>;
|
|
13
|
+
/**
|
|
14
|
+
* Returns the first row, or `Option.none()` for an empty result. Use it
|
|
15
|
+
* for a lookup whose shape is `Option`.
|
|
16
|
+
*/
|
|
17
|
+
export declare const firstRow: <A>(rows: readonly A[]) => Option.Option<A>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Effect, Option } from 'effect';
|
|
2
|
+
import { isUniqueViolation } from './sql-failure.js';
|
|
3
|
+
/**
|
|
4
|
+
* Splits a write path at the database boundary: a unique violation
|
|
5
|
+
* becomes a domain conflict, any other SqlError becomes "store
|
|
6
|
+
* unavailable". Pass a `unique` branch that builds the conflict failure
|
|
7
|
+
* and an `unavailable` branch, `portUnavailable` bound to `operation`.
|
|
8
|
+
*/
|
|
9
|
+
export const onUniqueViolation = (operation, branches) => (effect) => effect.pipe(Effect.catchTag('SqlError', (error) => isUniqueViolation(error)
|
|
10
|
+
? Effect.fail(branches.unique())
|
|
11
|
+
: Effect.fail(branches.unavailable(operation)(error))));
|
|
12
|
+
/**
|
|
13
|
+
* Returns the first row, or `Option.none()` for an empty result. Use it
|
|
14
|
+
* for a lookup whose shape is `Option`.
|
|
15
|
+
*/
|
|
16
|
+
export const firstRow = (rows) => Option.fromNullable(rows[0]);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SqlClient } from '@effect/sql';
|
|
2
|
+
import { TransactionBoundaryUnavailableError } from '@satorio/machinery/ports';
|
|
3
|
+
import { Effect } from 'effect';
|
|
4
|
+
/**
|
|
5
|
+
* Builds the boundary on a real Postgres transaction: a `SqlError` on
|
|
6
|
+
* the way in or out becomes the port's own unavailability failure, and
|
|
7
|
+
* the work's own failures pass through untouched.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export declare const makePgTransactionBoundary: Effect.Effect<{
|
|
12
|
+
inTransaction: <A, E, R>(work: Effect.Effect<A, E, R>) => Effect.Effect<A, E | TransactionBoundaryUnavailableError, R>;
|
|
13
|
+
}, never, SqlClient.SqlClient>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SqlClient } from '@effect/sql';
|
|
2
|
+
import { SqlError } from '@effect/sql/SqlError';
|
|
3
|
+
import { TransactionBoundaryUnavailableError, } from '@satorio/machinery/ports';
|
|
4
|
+
import { Effect } from 'effect';
|
|
5
|
+
/**
|
|
6
|
+
* Builds the boundary on a real Postgres transaction: a `SqlError` on
|
|
7
|
+
* the way in or out becomes the port's own unavailability failure, and
|
|
8
|
+
* the work's own failures pass through untouched.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export const makePgTransactionBoundary = Effect.gen(function* () {
|
|
13
|
+
const sql = yield* SqlClient.SqlClient;
|
|
14
|
+
const inTransaction = (work) => sql.withTransaction(work).pipe(Effect.mapError((failure) => failure instanceof SqlError
|
|
15
|
+
? new TransactionBoundaryUnavailableError({
|
|
16
|
+
operation: 'inTransaction',
|
|
17
|
+
cause: failure,
|
|
18
|
+
})
|
|
19
|
+
: failure));
|
|
20
|
+
return { inTransaction };
|
|
21
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type Refusal } from '@satorio/machinery/core';
|
|
2
|
+
import { type EventPublisherShape, type OutboxMessage } from '@satorio/machinery/ports';
|
|
3
|
+
import { Effect, type Scope } from 'effect';
|
|
4
|
+
import type { SnsPublisherSettings } from './settings.js';
|
|
5
|
+
/**
|
|
6
|
+
* What one publish call asks for.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export interface SnsPublishRequest {
|
|
11
|
+
readonly TopicArn: string;
|
|
12
|
+
readonly Message: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The narrowest slice of the SDK the publisher uses: put one message on
|
|
16
|
+
* the topic. A test can substitute its own object of this shape.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export interface SnsPublishTransport {
|
|
21
|
+
readonly publish: (request: SnsPublishRequest, signal?: AbortSignal) => Promise<unknown>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The transport plus the one call that closes the client behind it.
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export interface ClosableSnsTransport extends SnsPublishTransport {
|
|
29
|
+
readonly close: () => void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The message body a subscriber reads: the same envelope as the stdout
|
|
33
|
+
* line, minus the channel — here the topic itself plays that role.
|
|
34
|
+
*/
|
|
35
|
+
export declare const snsMessageBodyOf: (message: OutboxMessage) => string;
|
|
36
|
+
/**
|
|
37
|
+
* The publisher over one SNS transport: every message goes to the topic
|
|
38
|
+
* this prefix named, one publish call each.
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export declare const makeSnsEventPublisher: (transport: SnsPublishTransport, settings: SnsPublisherSettings) => EventPublisherShape;
|
|
43
|
+
/**
|
|
44
|
+
* Holds one transport open for the length of a scope and closes it when
|
|
45
|
+
* the scope ends.
|
|
46
|
+
*
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
export declare const scopedSnsTransport: (openTransport: () => ClosableSnsTransport) => Effect.Effect<SnsPublishTransport, never, Scope.Scope>;
|
|
50
|
+
/**
|
|
51
|
+
* Opens the real SNS client. This is the only place the SDK is
|
|
52
|
+
* constructed.
|
|
53
|
+
*
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
export declare const openSnsTransport: (settings: SnsPublisherSettings) => ClosableSnsTransport;
|
|
57
|
+
/**
|
|
58
|
+
* Without a topic set, the publisher does not fail service startup: it
|
|
59
|
+
* mounts a stand that fails `publish` loudly with the same three-part
|
|
60
|
+
* refusal the mount desk logged. The rest of the service keeps running,
|
|
61
|
+
* and the outbox relay reports the missing topic honestly instead of
|
|
62
|
+
* silently losing events.
|
|
63
|
+
*
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
export declare const eventPublisherUnavailableWithoutConfiguration: (refusal: Refusal) => EventPublisherShape;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { PublishCommand, SNSClient } from '@aws-sdk/client-sns';
|
|
2
|
+
import { bigIntsAsDecimalStrings, refusalText, } from '@satorio/machinery/core';
|
|
3
|
+
import { EventPublisherUnavailableError, } from '@satorio/machinery/ports';
|
|
4
|
+
import { DateTime, Effect, Option } from 'effect';
|
|
5
|
+
/**
|
|
6
|
+
* The message body a subscriber reads: the same envelope as the stdout
|
|
7
|
+
* line, minus the channel — here the topic itself plays that role.
|
|
8
|
+
*/
|
|
9
|
+
export const snsMessageBodyOf = (message) => JSON.stringify({
|
|
10
|
+
messageId: message.messageId,
|
|
11
|
+
eventType: message.eventType,
|
|
12
|
+
schemaVersion: message.schemaVersion,
|
|
13
|
+
occurredAt: DateTime.formatIso(message.occurredAt),
|
|
14
|
+
payload: message.payload,
|
|
15
|
+
}, bigIntsAsDecimalStrings);
|
|
16
|
+
/**
|
|
17
|
+
* The publisher over one SNS transport: every message goes to the topic
|
|
18
|
+
* this prefix named, one publish call each.
|
|
19
|
+
*
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
export const makeSnsEventPublisher = (transport, settings) => ({
|
|
23
|
+
publish: (messages) => Effect.forEach(messages, (message) => Effect.tryPromise({
|
|
24
|
+
try: (signal) => transport.publish({
|
|
25
|
+
TopicArn: settings.topicArn,
|
|
26
|
+
Message: snsMessageBodyOf(message),
|
|
27
|
+
}, signal),
|
|
28
|
+
catch: (cause) => new EventPublisherUnavailableError({ operation: 'publish', cause }),
|
|
29
|
+
}), { discard: true }),
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* Holds one transport open for the length of a scope and closes it when
|
|
33
|
+
* the scope ends.
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export const scopedSnsTransport = (openTransport) => Effect.acquireRelease(Effect.sync(openTransport), (transport) => Effect.sync(() => transport.close()));
|
|
38
|
+
const clientConfigurationOf = (settings) => ({
|
|
39
|
+
...Option.match(settings.region, {
|
|
40
|
+
onNone: () => ({}),
|
|
41
|
+
onSome: (clientRegion) => ({ region: clientRegion }),
|
|
42
|
+
}),
|
|
43
|
+
...Option.match(settings.endpoint, {
|
|
44
|
+
onNone: () => ({}),
|
|
45
|
+
onSome: (clientEndpoint) => ({ endpoint: clientEndpoint }),
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* Opens the real SNS client. This is the only place the SDK is
|
|
50
|
+
* constructed.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export const openSnsTransport = (settings) => {
|
|
55
|
+
const client = new SNSClient(clientConfigurationOf(settings));
|
|
56
|
+
return {
|
|
57
|
+
publish: (request, signal) => client.send(new PublishCommand(request), signal === undefined ? {} : { abortSignal: signal }),
|
|
58
|
+
close: () => client.destroy(),
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Without a topic set, the publisher does not fail service startup: it
|
|
63
|
+
* mounts a stand that fails `publish` loudly with the same three-part
|
|
64
|
+
* refusal the mount desk logged. The rest of the service keeps running,
|
|
65
|
+
* and the outbox relay reports the missing topic honestly instead of
|
|
66
|
+
* silently losing events.
|
|
67
|
+
*
|
|
68
|
+
* @internal
|
|
69
|
+
*/
|
|
70
|
+
export const eventPublisherUnavailableWithoutConfiguration = (refusal) => ({
|
|
71
|
+
publish: () => Effect.fail(new EventPublisherUnavailableError({
|
|
72
|
+
operation: 'publish',
|
|
73
|
+
cause: refusalText(refusal),
|
|
74
|
+
})),
|
|
75
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EventPublisherShape, OutboxMessage } from '@satorio/machinery/ports';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* The simplest publisher: an event travels as one JSON line on stdout,
|
|
5
|
+
* where a log collector picks it up. It proves the outbox relay works
|
|
6
|
+
* before a real message bus exists.
|
|
7
|
+
*/
|
|
8
|
+
export declare const stdoutLineOf: (channel: string, message: OutboxMessage) => string;
|
|
9
|
+
/**
|
|
10
|
+
* The publisher over one line writer: one line per message, in the order
|
|
11
|
+
* the batch holds them.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export declare const makeStdoutEventPublisher: (channel: string, writeLine: (line: string) => Effect.Effect<void>) => EventPublisherShape;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { bigIntsAsDecimalStrings } from '@satorio/machinery/core';
|
|
2
|
+
import { DateTime, Effect } from 'effect';
|
|
3
|
+
/**
|
|
4
|
+
* The simplest publisher: an event travels as one JSON line on stdout,
|
|
5
|
+
* where a log collector picks it up. It proves the outbox relay works
|
|
6
|
+
* before a real message bus exists.
|
|
7
|
+
*/
|
|
8
|
+
export const stdoutLineOf = (channel, message) => JSON.stringify({
|
|
9
|
+
channel,
|
|
10
|
+
messageId: message.messageId,
|
|
11
|
+
eventType: message.eventType,
|
|
12
|
+
schemaVersion: message.schemaVersion,
|
|
13
|
+
occurredAt: DateTime.formatIso(message.occurredAt),
|
|
14
|
+
payload: message.payload,
|
|
15
|
+
}, bigIntsAsDecimalStrings);
|
|
16
|
+
/**
|
|
17
|
+
* The publisher over one line writer: one line per message, in the order
|
|
18
|
+
* the batch holds them.
|
|
19
|
+
*
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
export const makeStdoutEventPublisher = (channel, writeLine) => ({
|
|
23
|
+
publish: (messages) => Effect.forEach(messages, (message) => writeLine(stdoutLineOf(channel, message)), { discard: true }),
|
|
24
|
+
});
|