better-ship 0.15.0 → 0.16.0
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/dist/{application-C4JOf71r.js → application-Cf9SER-i.js} +2 -2
- package/dist/{application-C4JOf71r.js.map → application-Cf9SER-i.js.map} +1 -1
- package/dist/application.d.ts +2 -2
- package/dist/application.js +1 -1
- package/dist/better-auth/apple-client-secret.d.ts +1 -1
- package/dist/better-auth/apple-client-secret.js +1 -1
- package/dist/better-auth/auth-client.js +1 -1
- package/dist/better-auth/rate-limit.d.ts +1 -1
- package/dist/better-auth/rate-limit.js +1 -1
- package/dist/better-auth.js +1 -1
- package/dist/cloudflare/auth-secondary-storage.d.ts +1 -1
- package/dist/cloudflare/auth-secondary-storage.js +1 -1
- package/dist/cloudflare/durable-object-client.js +1 -1
- package/dist/cloudflare/durable-object-errors.d.ts +1 -1
- package/dist/cloudflare/durable-object-errors.js +1 -1
- package/dist/cloudflare/queue.d.ts +2 -2
- package/dist/cloudflare/queue.js +2 -2
- package/dist/cloudflare/scheduled.d.ts +1 -1
- package/dist/cloudflare/scheduled.js +1 -1
- package/dist/cloudflare.d.ts +3 -3
- package/dist/cloudflare.js +3 -3
- package/dist/{core-hdaNiCOU.js → core-ClSP5-V9.js} +94 -44
- package/dist/core-ClSP5-V9.js.map +1 -0
- package/dist/core.d.ts +2 -2
- package/dist/core.js +2 -2
- package/dist/http.d.ts +2 -2
- package/dist/http.js +2 -2
- package/dist/{index-B45Dr5gs.d.ts → index-Cchdg2aQ.d.ts} +2 -2
- package/dist/{index-B45Dr5gs.d.ts.map → index-Cchdg2aQ.d.ts.map} +1 -1
- package/dist/{index-G1QG0P5g.d.ts → index-DvJUcSW1.d.ts} +102 -48
- package/dist/{index-G1QG0P5g.d.ts.map → index-DvJUcSW1.d.ts.map} +1 -1
- package/dist/{outbox-repository-options-CwTTuy7B.d.ts → outbox-repository-options-AkihEaik.d.ts} +3 -3
- package/dist/{outbox-repository-options-CwTTuy7B.d.ts.map → outbox-repository-options-AkihEaik.d.ts.map} +1 -1
- package/dist/{outbox-repository-options-DOTpJH-e.js → outbox-repository-options-ICOF8g2h.js} +2 -2
- package/dist/{outbox-repository-options-DOTpJH-e.js.map → outbox-repository-options-ICOF8g2h.js.map} +1 -1
- package/dist/postgres.d.ts +3 -3
- package/dist/postgres.js +3 -3
- package/dist/posthog/metrics.d.ts +1 -1
- package/dist/posthog/server.d.ts +2 -2
- package/dist/posthog/server.js +1 -1
- package/dist/resend.d.ts +3 -3
- package/dist/resend.js +2 -2
- package/dist/sentry/client.d.ts +1 -1
- package/dist/sentry/client.js +1 -1
- package/dist/sentry/server.d.ts +2 -2
- package/dist/tanstack.d.ts +1 -1
- package/dist/tanstack.js +1 -1
- package/dist/testing/outbox-repository.d.ts +2 -2
- package/dist/{webhook-verification-D7DIhH6C.d.ts → webhook-verification-BcQQG-vY.d.ts} +2 -2
- package/dist/{webhook-verification-D7DIhH6C.d.ts.map → webhook-verification-BcQQG-vY.d.ts.map} +1 -1
- package/dist/{webhook-verification-trEgyz7I.js → webhook-verification-D6aGQfNi.js} +2 -2
- package/dist/{webhook-verification-trEgyz7I.js.map → webhook-verification-D6aGQfNi.js.map} +1 -1
- package/package.json +1 -1
- package/src/core/define-messages.ts +209 -103
- package/src/core/index.ts +3 -1
- package/src/core/messages.ts +1 -1
- package/dist/core-hdaNiCOU.js.map +0 -1
package/dist/postgres.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { A as IOutboxRepository, M as OutboxClaim, N as OutboxMessage, P as IMessageStore, k as DeadLetterReason, w as ITransaction } from "./index-
|
|
3
|
-
import { n as eventCollector, r as DrizzleSchema, t as OutboxRepositoryOptions } from "./outbox-repository-options-
|
|
1
|
+
import { X as MessageId, at as DatabaseFailure, it as DatabaseError, q as DomainEvent } from "./index-DvJUcSW1.js";
|
|
2
|
+
import { A as IOutboxRepository, M as OutboxClaim, N as OutboxMessage, P as IMessageStore, k as DeadLetterReason, w as ITransaction } from "./index-Cchdg2aQ.js";
|
|
3
|
+
import { n as eventCollector, r as DrizzleSchema, t as OutboxRepositoryOptions } from "./outbox-repository-options-AkihEaik.js";
|
|
4
4
|
import { ExtractTablesWithRelations } from "drizzle-orm";
|
|
5
5
|
import { PostgresJsDatabase, PostgresJsTransaction, drizzle } from "drizzle-orm/postgres-js";
|
|
6
6
|
//#region src/infrastructure/postgres/postgres-database-types.d.ts
|
package/dist/postgres.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { o as MessageAlreadyProcessedError } from "./application-
|
|
3
|
-
import { i as runWithUnitOfWorkContext, r as eventCollector, t as OUTBOX_REPOSITORY_DEFAULTS } from "./outbox-repository-options-
|
|
1
|
+
import { c as exponentialDelayCapMs, k as DatabaseError, rt as AppError } from "./core-ClSP5-V9.js";
|
|
2
|
+
import { o as MessageAlreadyProcessedError } from "./application-Cf9SER-i.js";
|
|
3
|
+
import { i as runWithUnitOfWorkContext, r as eventCollector, t as OUTBOX_REPOSITORY_DEFAULTS } from "./outbox-repository-options-ICOF8g2h.js";
|
|
4
4
|
import { DrizzleQueryError } from "drizzle-orm/errors";
|
|
5
5
|
import { and, asc, eq, inArray, isNull, lt, lte, or, sql } from "drizzle-orm";
|
|
6
6
|
import { index, integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { J as MetricsService, q as MetricOptions } from "../index-
|
|
1
|
+
import { J as MetricsService, q as MetricOptions } from "../index-Cchdg2aQ.js";
|
|
2
2
|
import { PostHog } from "posthog-node";
|
|
3
3
|
//#region src/infrastructure/posthog/metrics.d.ts
|
|
4
4
|
/** PostHog Metrics with Worker lifetime management, ported from Typist's metrics-service.ts. */
|
package/dist/posthog/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Jt as
|
|
2
|
-
import { X as AnalyticsService } from "../index-
|
|
1
|
+
import { Jt as AnalyticsEventIdentity, Xt as AnalyticsProperties, Yt as AnalyticsEventInput } from "../index-DvJUcSW1.js";
|
|
2
|
+
import { X as AnalyticsService } from "../index-Cchdg2aQ.js";
|
|
3
3
|
import { PostHogMetricsService } from "./metrics.js";
|
|
4
4
|
import { PostHog, PostHogOptions } from "posthog-node";
|
|
5
5
|
//#region src/infrastructure/posthog/server.d.ts
|
package/dist/posthog/server.js
CHANGED
package/dist/resend.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { B as OutboundEmail, I as EmailAccepted, R as IEmailService } from "./index-
|
|
3
|
-
import { r as WebhookVerifier, t as WebhookVerification } from "./webhook-verification-
|
|
1
|
+
import { Pt as BounceType } from "./index-DvJUcSW1.js";
|
|
2
|
+
import { B as OutboundEmail, I as EmailAccepted, R as IEmailService } from "./index-Cchdg2aQ.js";
|
|
3
|
+
import { r as WebhookVerifier, t as WebhookVerification } from "./webhook-verification-BcQQG-vY.js";
|
|
4
4
|
import { Resend } from "resend";
|
|
5
5
|
//#region src/infrastructure/resend/resend-email-service.d.ts
|
|
6
6
|
/** The SDK operations required by the adapter; a test factory can return a plain object. */
|
package/dist/resend.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as WebhookVerificationError } from "./webhook-verification-
|
|
1
|
+
import { Y as parseBounceType, Z as EmailServiceError, et as EmailAddressSchema } from "./core-ClSP5-V9.js";
|
|
2
|
+
import { t as WebhookVerificationError } from "./webhook-verification-D6aGQfNi.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { Buffer } from "node:buffer";
|
|
5
5
|
|
package/dist/sentry/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { f as LoggerErrorHook } from "../index-
|
|
1
|
+
import { f as LoggerErrorHook } from "../index-DvJUcSW1.js";
|
|
2
2
|
import * as Sentry from "@sentry/tanstackstart-react";
|
|
3
3
|
//#region src/infrastructure/sentry/client.d.ts
|
|
4
4
|
/** Application settings use Sentry's names; enablement and environment belong to the application. */
|
package/dist/sentry/client.js
CHANGED
package/dist/sentry/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { f as LoggerErrorHook } from "../index-
|
|
2
|
-
import { H as ObservabilityScope, U as ObservabilityService, W as ObservabilityUser } from "../index-
|
|
1
|
+
import { f as LoggerErrorHook } from "../index-DvJUcSW1.js";
|
|
2
|
+
import { H as ObservabilityScope, U as ObservabilityService, W as ObservabilityUser } from "../index-Cchdg2aQ.js";
|
|
3
3
|
import * as Sentry from "@sentry/cloudflare";
|
|
4
4
|
//#region src/infrastructure/sentry/server.d.ts
|
|
5
5
|
/** Application-owned deployment settings with the full Cloudflare SDK option contract. */
|
package/dist/tanstack.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lt as ErrorTable } from "./index-DvJUcSW1.js";
|
|
2
2
|
import { Register } from "@tanstack/react-start";
|
|
3
3
|
import { BetterAuthCookies } from "better-auth";
|
|
4
4
|
//#region src/infrastructure/tanstack/request-context.d.ts
|
package/dist/tanstack.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { G as AuthenticationError, U as StaleSessionError, a as createLogger, rt as AppError, z as ValidationError } from "./core-ClSP5-V9.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getReasonPhrase } from "http-status-codes";
|
|
4
4
|
import { isNotFound, isRedirect, redirect } from "@tanstack/react-router";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { A as IOutboxRepository } from "../index-
|
|
1
|
+
import { q as DomainEvent } from "../index-DvJUcSW1.js";
|
|
2
|
+
import { A as IOutboxRepository } from "../index-Cchdg2aQ.js";
|
|
3
3
|
//#region src/infrastructure/testing/outbox-repository-contract.d.ts
|
|
4
4
|
/** Stored fields observed by the adapter contract, independent of a database library. */
|
|
5
5
|
export type OutboxContractRow = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $t as AppError } from "./index-DvJUcSW1.js";
|
|
2
2
|
//#region src/infrastructure/http/webhook-verification.d.ts
|
|
3
3
|
/** A webhook cannot be trusted or parsed. The HTTP handler logs the rejection once. */
|
|
4
4
|
declare class WebhookVerificationError extends AppError {
|
|
@@ -26,4 +26,4 @@ interface WebhookVerifier<Event> {
|
|
|
26
26
|
}
|
|
27
27
|
//#endregion
|
|
28
28
|
export { WebhookVerificationError as n, WebhookVerifier as r, WebhookVerification as t };
|
|
29
|
-
//# sourceMappingURL=webhook-verification-
|
|
29
|
+
//# sourceMappingURL=webhook-verification-BcQQG-vY.d.ts.map
|
package/dist/{webhook-verification-D7DIhH6C.d.ts.map → webhook-verification-BcQQG-vY.d.ts.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook-verification-
|
|
1
|
+
{"version":3,"file":"webhook-verification-BcQQG-vY.d.ts","names":[],"sources":["../src/infrastructure/http/webhook-verification.ts"],"mappings":";;;cAGa,iCAAiC;WAMjC;WALF;WACA;;EAGT,YACW,qEACT;;;KAOQ,oBAAoB;WACjB;WAA2B,OAAO;;WAClC;WAA6B;;WAC7B;WAA8B,OAAO;;;UAGnC,gBAAgB;;EAE/B,OAAO,cAAc,SAAS,UAAU,QAAQ,oBAAoB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { rt as AppError } from "./core-ClSP5-V9.js";
|
|
2
2
|
|
|
3
3
|
//#region src/infrastructure/http/webhook-verification.ts
|
|
4
4
|
/** A webhook cannot be trusted or parsed. The HTTP handler logs the rejection once. */
|
|
@@ -15,4 +15,4 @@ var WebhookVerificationError = class extends AppError {
|
|
|
15
15
|
|
|
16
16
|
//#endregion
|
|
17
17
|
export { WebhookVerificationError as t };
|
|
18
|
-
//# sourceMappingURL=webhook-verification-
|
|
18
|
+
//# sourceMappingURL=webhook-verification-D6aGQfNi.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook-verification-
|
|
1
|
+
{"version":3,"file":"webhook-verification-D6aGQfNi.js","names":[],"sources":["../src/infrastructure/http/webhook-verification.ts"],"sourcesContent":["import { AppError } from '@/core'\n\n/** A webhook cannot be trusted or parsed. The HTTP handler logs the rejection once. */\nexport class WebhookVerificationError extends AppError {\n readonly _tag = 'WebhookVerificationError'\n readonly classification = 'terminal'\n\n /** Keep secrets and payload details out of the public message. */\n constructor(\n readonly reason: 'missing_headers' | 'invalid_signature' | 'invalid_payload',\n cause?: unknown,\n ) {\n super(`Webhook rejected: ${reason}`, { cause })\n }\n}\n\n/** A verified event, an ignored event, or an invalid request. */\nexport type WebhookVerification<Event> =\n | { readonly outcome: 'event'; readonly event: Event }\n | { readonly outcome: 'ignored'; readonly type: string }\n | { readonly outcome: 'rejected'; readonly error: WebhookVerificationError }\n\n/** The provider owns signature verification, timestamp tolerance, and payload parsing. */\nexport interface WebhookVerifier<Event> {\n /** Verify the exact body before parsing it. Return invalid requests; throw operational failures. */\n verify(body: string, headers: Headers): Promise<WebhookVerification<Event>>\n}\n"],"mappings":";;;;AAGA,IAAa,2BAAb,cAA8C,SAAS;CAM1C;CALX,AAAS,OAAO;CAChB,AAAS,iBAAiB;;CAG1B,YACE,AAAS,QACT,OACA;EACA,MAAM,qBAAqB,UAAU,EAAE,MAAM,CAAC;EAHrC;CAIX;AACF"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://www.schemastore.org/package.json",
|
|
3
3
|
"name": "better-ship",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.16.0",
|
|
5
5
|
"description": "Foundation for applications on Cloudflare Workers: logger, errors, message bus, infrastructure adapters, and UI primitives",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -1,136 +1,242 @@
|
|
|
1
|
+
import { panic } from 'better-result'
|
|
1
2
|
import { v7 as uuidv7 } from 'uuid'
|
|
2
3
|
import { z } from 'zod'
|
|
3
4
|
|
|
4
|
-
import
|
|
5
|
+
import { AppError } from './app-error.ts'
|
|
6
|
+
import type { MessageId } from './messages.ts'
|
|
5
7
|
|
|
6
|
-
/** Payload schemas keyed by message name.
|
|
8
|
+
/** Payload schemas keyed by message name. Payloads cannot declare `type`, `name`, or `id`. */
|
|
7
9
|
export type PayloadSchemas = Readonly<Record<string, z.ZodType<object>>>
|
|
8
10
|
|
|
9
|
-
/**
|
|
11
|
+
/** Optional payload schema maps for one message collection. */
|
|
10
12
|
export type MessageDefinition = {
|
|
11
|
-
readonly commands
|
|
12
|
-
readonly events
|
|
13
|
-
readonly queries
|
|
13
|
+
readonly commands?: PayloadSchemas
|
|
14
|
+
readonly events?: PayloadSchemas
|
|
15
|
+
readonly queries?: PayloadSchemas
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Payload<Def<Messages>['commands'], Name>
|
|
24
|
-
}[keyof Def<Messages>['commands'] & string]
|
|
25
|
-
|
|
26
|
-
/** Every event of an application: `EventOf<typeof messages>`. */
|
|
27
|
-
export type EventOf<Messages extends AnyMessages> = {
|
|
28
|
-
[Name in keyof Def<Messages>['events'] & string]: DomainEvent<Name> &
|
|
29
|
-
Payload<Def<Messages>['events'], Name>
|
|
30
|
-
}[keyof Def<Messages>['events'] & string]
|
|
31
|
-
|
|
32
|
-
/** Every query of an application: `QueryOf<typeof messages>`. */
|
|
33
|
-
export type QueryOf<Messages extends AnyMessages> = {
|
|
34
|
-
[Name in keyof Def<Messages>['queries'] & string]: DomainQuery<Name> &
|
|
35
|
-
Payload<Def<Messages>['queries'], Name>
|
|
36
|
-
}[keyof Def<Messages>['queries'] & string]
|
|
37
|
-
|
|
38
|
-
/** Every message of an application: what the registry and the bus are typed with. */
|
|
39
|
-
export type MessageOf<Messages extends AnyMessages> =
|
|
40
|
-
| CommandOf<Messages>
|
|
41
|
-
| EventOf<Messages>
|
|
42
|
-
| QueryOf<Messages>
|
|
18
|
+
const completed = Symbol('MessageCollection')
|
|
19
|
+
const MessageIdSchema = z.string().min(1)
|
|
20
|
+
const envelopes = {
|
|
21
|
+
command: z.object({ type: z.literal('command'), id: MessageIdSchema }),
|
|
22
|
+
event: z.object({ type: z.literal('event'), id: MessageIdSchema }),
|
|
23
|
+
query: z.object({ type: z.literal('query') }),
|
|
24
|
+
}
|
|
43
25
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
readonly
|
|
50
|
-
|
|
51
|
-
|
|
26
|
+
type MessageKind = keyof typeof envelopes
|
|
27
|
+
type RoutingField = 'type' | 'name' | 'id'
|
|
28
|
+
type SchemaMap = Readonly<Record<string, z.ZodType<object>>>
|
|
29
|
+
type EmptySchemas = Record<never, never>
|
|
30
|
+
type SchemaMaps = {
|
|
31
|
+
readonly commandSchemas: SchemaMap
|
|
32
|
+
readonly eventSchemas: SchemaMap
|
|
33
|
+
readonly querySchemas: SchemaMap
|
|
34
|
+
}
|
|
35
|
+
type CompletedMessages = SchemaMaps & { readonly [completed]: true }
|
|
36
|
+
type SchemaOf<Schemas extends SchemaMap> = Schemas[keyof Schemas & string]
|
|
37
|
+
type Payload<Message> = Message extends object ? Omit<Message, RoutingField> : never
|
|
38
|
+
|
|
39
|
+
/** Complete schemas, union schemas, and factories for one message collection. */
|
|
40
|
+
export type MessageCollection<Maps extends SchemaMaps> = Maps & {
|
|
41
|
+
readonly [completed]: true
|
|
42
|
+
readonly commandSchema: z.ZodUnion<SchemaOf<Maps['commandSchemas']>[]>
|
|
43
|
+
readonly eventSchema: z.ZodUnion<SchemaOf<Maps['eventSchemas']>[]>
|
|
44
|
+
readonly querySchema: z.ZodUnion<SchemaOf<Maps['querySchemas']>[]>
|
|
45
|
+
/** Commands and events can cross a queue. Queries cannot. */
|
|
46
|
+
readonly queueSchema: z.ZodUnion<
|
|
47
|
+
(SchemaOf<Maps['commandSchemas']> | SchemaOf<Maps['eventSchemas']>)[]
|
|
48
|
+
>
|
|
49
|
+
/** Add the command envelope without parsing. The default ID is UUID v7; supplied IDs stay unchanged. */
|
|
50
|
+
createCommand<Name extends keyof Maps['commandSchemas'] & string>(
|
|
51
|
+
this: void,
|
|
52
52
|
name: Name,
|
|
53
|
-
payload: Payload<
|
|
53
|
+
payload: Payload<z.output<Maps['commandSchemas'][NoInfer<Name>]>>,
|
|
54
54
|
id?: MessageId,
|
|
55
|
-
):
|
|
56
|
-
/**
|
|
57
|
-
createEvent<Name extends keyof
|
|
55
|
+
): z.output<Maps['commandSchemas'][Name]>
|
|
56
|
+
/** Add the event envelope without parsing. The default ID is UUID v7; supplied IDs stay unchanged. */
|
|
57
|
+
createEvent<Name extends keyof Maps['eventSchemas'] & string>(
|
|
58
|
+
this: void,
|
|
58
59
|
name: Name,
|
|
59
|
-
payload: Payload<
|
|
60
|
+
payload: Payload<z.output<Maps['eventSchemas'][NoInfer<Name>]>>,
|
|
60
61
|
id?: MessageId,
|
|
61
|
-
):
|
|
62
|
-
/**
|
|
63
|
-
createQuery<Name extends keyof
|
|
62
|
+
): z.output<Maps['eventSchemas'][Name]>
|
|
63
|
+
/** Add the query envelope without parsing. Queries have no ID. */
|
|
64
|
+
createQuery<Name extends keyof Maps['querySchemas'] & string>(
|
|
65
|
+
this: void,
|
|
64
66
|
name: Name,
|
|
65
|
-
payload: Payload<
|
|
66
|
-
):
|
|
67
|
-
readonly commandSchema: z.ZodType<CommandOf<AppMessages<Definition>>>
|
|
68
|
-
readonly eventSchema: z.ZodType<EventOf<AppMessages<Definition>>>
|
|
69
|
-
readonly querySchema: z.ZodType<QueryOf<AppMessages<Definition>>>
|
|
70
|
-
/** What a queue body may be: a command or an event. Queries never cross a queue. */
|
|
71
|
-
readonly queueSchema: z.ZodType<
|
|
72
|
-
CommandOf<AppMessages<Definition>> | EventOf<AppMessages<Definition>>
|
|
73
|
-
>
|
|
67
|
+
payload: Payload<z.output<Maps['querySchemas'][NoInfer<Name>]>>,
|
|
68
|
+
): z.output<Maps['querySchemas'][Name]>
|
|
74
69
|
}
|
|
75
70
|
|
|
76
|
-
|
|
71
|
+
/** The command union inferred from a collection's complete schemas. */
|
|
72
|
+
export type CommandOf<Messages extends CompletedMessages> = z.output<
|
|
73
|
+
SchemaOf<Messages['commandSchemas']>
|
|
74
|
+
>
|
|
75
|
+
|
|
76
|
+
/** The event union inferred from a collection's complete schemas. */
|
|
77
|
+
export type EventOf<Messages extends CompletedMessages> = z.output<
|
|
78
|
+
SchemaOf<Messages['eventSchemas']>
|
|
79
|
+
>
|
|
77
80
|
|
|
78
|
-
/**
|
|
81
|
+
/** The query union inferred from a collection's complete schemas. */
|
|
82
|
+
export type QueryOf<Messages extends CompletedMessages> = z.output<
|
|
83
|
+
SchemaOf<Messages['querySchemas']>
|
|
84
|
+
>
|
|
85
|
+
|
|
86
|
+
/** Every message inferred from a collection's complete schemas. */
|
|
87
|
+
export type MessageOf<Messages extends CompletedMessages> =
|
|
88
|
+
| CommandOf<Messages>
|
|
89
|
+
| EventOf<Messages>
|
|
90
|
+
| QueryOf<Messages>
|
|
91
|
+
|
|
92
|
+
type KeysOfUnion<Value> = Value extends Value ? keyof Value : never
|
|
79
93
|
type WithoutRoutingFields<Schemas extends PayloadSchemas> = {
|
|
80
|
-
readonly [Name in keyof Schemas]:
|
|
94
|
+
readonly [Name in keyof Schemas]: KeysOfUnion<z.output<Schemas[Name]>> &
|
|
95
|
+
RoutingField extends never
|
|
81
96
|
? Schemas[Name]
|
|
82
97
|
: never
|
|
83
98
|
}
|
|
84
99
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
function messageSchema<
|
|
101
|
+
Kind extends MessageKind,
|
|
102
|
+
Name extends string,
|
|
103
|
+
Schema extends z.ZodType<object>,
|
|
104
|
+
>(kind: Kind, name: Name, payload: Schema) {
|
|
105
|
+
return z.intersection(envelopes[kind], z.object({ name: z.literal(name) })).and(payload)
|
|
90
106
|
}
|
|
91
107
|
|
|
92
|
-
|
|
108
|
+
type CompleteSchemas<Kind extends MessageKind, Schemas extends PayloadSchemas> = {
|
|
109
|
+
readonly [Name in keyof Schemas & string]: ReturnType<
|
|
110
|
+
typeof messageSchema<Kind, Name, Schemas[Name]>
|
|
111
|
+
>
|
|
112
|
+
}
|
|
93
113
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
function completeSchemas<Kind extends MessageKind, Schemas extends PayloadSchemas>(
|
|
115
|
+
kind: Kind,
|
|
116
|
+
schemas: Schemas | undefined,
|
|
117
|
+
): CompleteSchemas<Kind, Schemas> {
|
|
118
|
+
const entries = Object.entries<z.ZodType<object>>(schemas ?? {}).map(([name, payload]) => [
|
|
119
|
+
name,
|
|
120
|
+
messageSchema(kind, name, payload),
|
|
121
|
+
])
|
|
122
|
+
// SAFETY: Each entry preserves its name and wraps its payload schema with the selected envelope.
|
|
123
|
+
return Object.freeze(Object.fromEntries(entries)) as CompleteSchemas<Kind, Schemas>
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function unionOf<Schemas extends SchemaMap>(schemas: Schemas): z.ZodUnion<SchemaOf<Schemas>[]> {
|
|
127
|
+
// SAFETY: Object.values contains exactly the complete schemas indexed by this map's keys.
|
|
128
|
+
return z.union(Object.values(schemas)) as z.ZodUnion<SchemaOf<Schemas>[]>
|
|
105
129
|
}
|
|
106
130
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
131
|
+
function collection<Maps extends SchemaMaps>(maps: Maps): MessageCollection<Maps> {
|
|
132
|
+
return Object.freeze<MessageCollection<Maps>>({
|
|
133
|
+
...maps,
|
|
134
|
+
[completed]: true,
|
|
135
|
+
commandSchema: unionOf<Maps['commandSchemas']>(maps.commandSchemas),
|
|
136
|
+
eventSchema: unionOf<Maps['eventSchemas']>(maps.eventSchemas),
|
|
137
|
+
querySchema: unionOf<Maps['querySchemas']>(maps.querySchemas),
|
|
138
|
+
// SAFETY: The queue union contains the command and event schemas from these maps, without rebuilding them.
|
|
139
|
+
queueSchema: z.union([
|
|
140
|
+
...Object.values(maps.commandSchemas),
|
|
141
|
+
...Object.values(maps.eventSchemas),
|
|
142
|
+
]) as MessageCollection<Maps>['queueSchema'],
|
|
143
|
+
createCommand: (name, payload, id = uuidv7()) => {
|
|
144
|
+
// SAFETY: The typed payload supplies all non-envelope fields for this name; the factory supplies its envelope.
|
|
145
|
+
return { ...payload, type: 'command', name, id } as z.output<
|
|
146
|
+
Maps['commandSchemas'][typeof name]
|
|
147
|
+
>
|
|
148
|
+
},
|
|
149
|
+
createEvent: (name, payload, id = uuidv7()) => {
|
|
150
|
+
// SAFETY: The typed payload supplies all non-envelope fields for this name; the factory supplies its envelope.
|
|
151
|
+
return { ...payload, type: 'event', name, id } as z.output<Maps['eventSchemas'][typeof name]>
|
|
152
|
+
},
|
|
153
|
+
createQuery: (name, payload) => {
|
|
154
|
+
// SAFETY: The typed payload supplies all non-envelope fields for this name; a query has no ID.
|
|
155
|
+
return { ...payload, type: 'query', name } as z.output<Maps['querySchemas'][typeof name]>
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Build each complete schema once. Omitted kinds have empty maps and schemas that reject every input. */
|
|
161
|
+
export function defineMessages<
|
|
162
|
+
const Commands extends PayloadSchemas = EmptySchemas,
|
|
163
|
+
const Events extends PayloadSchemas = EmptySchemas,
|
|
164
|
+
const Queries extends PayloadSchemas = EmptySchemas,
|
|
165
|
+
>(definition: {
|
|
166
|
+
readonly commands?: Commands & WithoutRoutingFields<Commands>
|
|
167
|
+
readonly events?: Events & WithoutRoutingFields<Events>
|
|
168
|
+
readonly queries?: Queries & WithoutRoutingFields<Queries>
|
|
169
|
+
}) {
|
|
170
|
+
return collection({
|
|
171
|
+
commandSchemas: completeSchemas<'command', Commands>('command', definition.commands),
|
|
172
|
+
eventSchemas: completeSchemas<'event', Events>('event', definition.events),
|
|
173
|
+
querySchemas: completeSchemas<'query', Queries>('query', definition.queries),
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Two collections declare the same name within one message kind. */
|
|
178
|
+
export class DuplicateMessageNameError extends AppError {
|
|
179
|
+
readonly _tag = 'DuplicateMessageNameError'
|
|
180
|
+
readonly classification = 'terminal'
|
|
181
|
+
|
|
182
|
+
/** Identify the duplicate kind and name so the application can correct its definitions. */
|
|
183
|
+
constructor(
|
|
184
|
+
readonly kind: MessageKind,
|
|
185
|
+
readonly messageName: string,
|
|
186
|
+
) {
|
|
187
|
+
super(`Duplicate ${kind} name: ${messageName}`)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
type SchemaAtName<Maps, Name extends PropertyKey> =
|
|
192
|
+
Maps extends Record<Name, infer Schema extends z.ZodType<object>> ? Schema : never
|
|
193
|
+
type MergedMaps<Collections extends readonly CompletedMessages[]> = {
|
|
194
|
+
readonly [Key in keyof SchemaMaps]: {
|
|
195
|
+
readonly [Name in KeysOfUnion<Collections[number][Key]> & string]: SchemaAtName<
|
|
196
|
+
Collections[number][Key],
|
|
197
|
+
Name
|
|
198
|
+
>
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
type DuplicateNames<Collections extends readonly CompletedMessages[]> =
|
|
202
|
+
Collections extends readonly [
|
|
203
|
+
infer First extends CompletedMessages,
|
|
204
|
+
...infer Rest extends readonly CompletedMessages[],
|
|
205
|
+
]
|
|
206
|
+
?
|
|
207
|
+
| {
|
|
208
|
+
[Key in keyof SchemaMaps]: keyof First[Key] & KeysOfUnion<Rest[number][Key]>
|
|
209
|
+
}[keyof SchemaMaps]
|
|
210
|
+
| DuplicateNames<Rest>
|
|
211
|
+
: never
|
|
212
|
+
|
|
213
|
+
function mergeSchemas<
|
|
214
|
+
Collections extends readonly CompletedMessages[],
|
|
215
|
+
Key extends keyof SchemaMaps,
|
|
216
|
+
>(collections: Collections, key: Key, kind: MessageKind): MergedMaps<Collections>[Key] {
|
|
217
|
+
const merged = new Map<string, z.ZodType<object>>()
|
|
218
|
+
for (const messages of collections) {
|
|
219
|
+
if (!messages?.[completed])
|
|
220
|
+
return panic('combineMessages requires completed message collections')
|
|
221
|
+
for (const [name, schema] of Object.entries(messages[key])) {
|
|
222
|
+
if (merged.has(name)) throw new DuplicateMessageNameError(kind, name)
|
|
223
|
+
merged.set(name, schema)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// SAFETY: Every schema comes from the selected complete map, and duplicate names are rejected before insertion.
|
|
227
|
+
return Object.freeze(Object.fromEntries(merged)) as MergedMaps<Collections>[Key]
|
|
112
228
|
}
|
|
113
229
|
|
|
114
230
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
231
|
+
* Merge completed collections without rebuilding complete schemas.
|
|
232
|
+
* @throws DuplicateMessageNameError when two collections declare the same name within one kind.
|
|
117
233
|
*/
|
|
118
|
-
export function
|
|
119
|
-
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
createCommand: (name, payload, id = uuidv7()) => ({ ...payload, type: 'command', name, id }),
|
|
127
|
-
createEvent: (name, payload, id = uuidv7()) => ({ ...payload, type: 'event', name, id }),
|
|
128
|
-
createQuery: (name, payload) => ({ ...payload, type: 'query', name }),
|
|
129
|
-
commandSchema: commandSchema.pipe(z.custom<CommandOf<AppMessages<Definition>>>()),
|
|
130
|
-
eventSchema: eventSchema.pipe(z.custom<EventOf<AppMessages<Definition>>>()),
|
|
131
|
-
querySchema: querySchema.pipe(z.custom<QueryOf<AppMessages<Definition>>>()),
|
|
132
|
-
queueSchema: z
|
|
133
|
-
.union([commandSchema, eventSchema])
|
|
134
|
-
.pipe(z.custom<CommandOf<AppMessages<Definition>> | EventOf<AppMessages<Definition>>>()),
|
|
135
|
-
}
|
|
234
|
+
export function combineMessages<const Collections extends readonly CompletedMessages[]>(
|
|
235
|
+
...collections: Collections & (DuplicateNames<Collections> extends never ? unknown : never)
|
|
236
|
+
) {
|
|
237
|
+
return collection({
|
|
238
|
+
commandSchemas: mergeSchemas(collections, 'commandSchemas', 'command'),
|
|
239
|
+
eventSchemas: mergeSchemas(collections, 'eventSchemas', 'event'),
|
|
240
|
+
querySchemas: mergeSchemas(collections, 'querySchemas', 'query'),
|
|
241
|
+
})
|
|
136
242
|
}
|
package/src/core/index.ts
CHANGED
|
@@ -62,7 +62,9 @@ export { notImplemented, unreachable } from './defects.ts'
|
|
|
62
62
|
export { defineId, type Id, type IdDefinition, type IdOf } from './identity.ts'
|
|
63
63
|
export {
|
|
64
64
|
defineMessages,
|
|
65
|
-
|
|
65
|
+
combineMessages,
|
|
66
|
+
DuplicateMessageNameError,
|
|
67
|
+
type MessageCollection,
|
|
66
68
|
type CommandOf,
|
|
67
69
|
type EventOf,
|
|
68
70
|
type MessageDefinition,
|
package/src/core/messages.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Internal messages use UUID v7, so receipts sort by creation. External messages use the
|
|
5
5
|
* provider's own key, `stripe:event:evt_123`, so a redelivered webhook is a duplicate by
|
|
6
|
-
* construction.
|
|
6
|
+
* construction. Message factories generate IDs; complete message schemas parse external IDs.
|
|
7
7
|
*/
|
|
8
8
|
export type MessageId = string
|
|
9
9
|
|