Skip to content

Commit

Permalink
Clarify internal/external separation (#135)
Browse files Browse the repository at this point in the history
  • Loading branch information
kibertoad authored May 2, 2024
1 parent 07e3f3e commit 5d9c2a7
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 26 deletions.
2 changes: 2 additions & 0 deletions packages/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,13 @@ export type { MessageSchemaContainerOptions } from './lib/queues/MessageSchemaCo
export { objectToBuffer } from './lib/utils/queueUtils'
export { waitAndRetry } from './lib/utils/waitUtils'
export { parseMessage } from './lib/utils/parseUtils'
export { toDatePreprocessor } from './lib/utils/toDateProcessor'

export { reloadConfig, isProduction } from './lib/utils/envUtils'

export { DomainEventEmitter } from './lib/events/DomainEventEmitter'
export { EventRegistry } from './lib/events/EventRegistry'
export { FakeListener } from './lib/events/fakes/FakeListener'
export * from './lib/events/eventTypes'
export * from './lib/events/baseEventSchemas'
export * from './lib/messages/baseMessageSchemas'
11 changes: 11 additions & 0 deletions packages/core/lib/events/baseEventSchemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { z } from 'zod'

// Core fields that describe either internal event or external message
export const BASE_EVENT_SCHEMA = z.object({
id: z.string().describe('event unique identifier'),
timestamp: z.string().datetime().describe('iso 8601 datetime'),
type: z.literal<string>('<replace.me>').describe('event type name'),
payload: z.optional(z.object({})).describe('event payload based on type'),
})

export type BaseEventType = z.infer<typeof BASE_EVENT_SCHEMA>
2 changes: 1 addition & 1 deletion packages/core/lib/events/eventTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { BASE_MESSAGE_SCHEMA } from '../messages/baseMessageSchemas'
export type EventTypeNames<EventDefinition extends CommonEventDefinition> =
CommonEventDefinitionSchemaType<EventDefinition>['type']

// To be extended with transport-specific fields, e. g. "snsTopic" in specific libraries
// To be extended with transport-specific fields, e.g. "snsTopic" in specific libraries
export type CommonEventDefinition = {
schema: ZodObject<
Omit<(typeof BASE_MESSAGE_SCHEMA)['shape'], 'payload'> & { payload: ZodTypeAny }
Expand Down
44 changes: 22 additions & 22 deletions packages/core/lib/messages/baseMessageSchemas.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import z from 'zod'

// Core fields that describe event
export const BASE_MESSAGE_SCHEMA = z.object({
id: z.string().describe('event unique identifier'),
timestamp: z.string().datetime().describe('iso 8601 datetime'),
type: z.literal<string>('<replace.me>').describe('event type name'),
payload: z.optional(z.object({})).describe('event payload based on type'),
})
import { BASE_EVENT_SCHEMA } from '../events/baseEventSchemas'

// External message metadata that describe the context in which the message was created, primarily used for debugging purposes
export const MESSAGE_METADATA_SCHEMA = z
.object({
schemaVersion: z.string().min(1).describe('message schema version'),
// this is always set to a service that created the message
producedBy: z.string().min(1).describe('app/service that produced the message'),
// this is always propagated within the message chain. For the first message in the chain it is equal to "producedBy"
originatedFrom: z
.string()
.min(1)
.describe('app/service that initiated entire workflow that led to creating this message'),
// this is always propagated within the message chain.
correlationId: z.string().describe('unique identifier passed to all events in workflow chain'),
})
.describe('external message metadata')

// Extra fields that are optional for the event processing
export const EXTENDED_MESSAGE_SCHEMA = BASE_MESSAGE_SCHEMA.extend({
metadata: z
.object({
schemaVersion: z.string().min(1).describe('message schema version'),
producedBy: z.string().min(1).describe('app/service that produced the message'),
originatedFrom: z
.string()
.min(1)
.describe('app/service that initiated entire workflow that led to creating this message'),
correlationId: z
.string()
.describe('unique identifier passed to all events in workflow chain'),
})
.describe('event metadata'),
export const BASE_MESSAGE_SCHEMA = BASE_EVENT_SCHEMA.extend({
// For internal domain events that did not originate within a message chain metadata field can be omitted, producer should then assume it is initiating a new chain
metadata: MESSAGE_METADATA_SCHEMA.optional(),
})

export type BaseMessageType = z.infer<typeof BASE_MESSAGE_SCHEMA>

export type MessageMetadataType = z.infer<typeof MESSAGE_METADATA_SCHEMA>
52 changes: 52 additions & 0 deletions packages/core/lib/utils/toDatePreprocessor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, expect, it } from 'vitest'
import { z } from 'zod'

import { toDatePreprocessor } from './toDateProcessor'

describe('toDatePreprocessor', () => {
it('converts valid strings to date', () => {
const SCHEMA = z.object({
createdAt: z.preprocess(toDatePreprocessor, z.date()),
updatedAt: z.preprocess(toDatePreprocessor, z.date()),
})

const result = SCHEMA.parse({
createdAt: '2016-01-01',
updatedAt: '2022-01-12T00:00:00.000Z',
})

expect(result).toEqual({
createdAt: new Date('2016-01-01'),
updatedAt: new Date('2022-01-12T00:00:00.000Z'),
})
})

it('converts valid numbers to date', () => {
const SCHEMA = z.object({
createdAt: z.preprocess(toDatePreprocessor, z.date()),
updatedAt: z.preprocess(toDatePreprocessor, z.date()),
})

const result = SCHEMA.parse({
createdAt: 166,
updatedAt: 99999,
})

expect(result).toEqual({
createdAt: new Date(166),
updatedAt: new Date(99999),
})
})

it('does not convert function input', () => {
const SCHEMA = z.object({
createdAt: z.preprocess(toDatePreprocessor, z.date()),
})

expect(() =>
SCHEMA.parse({
createdAt: (x: string) => x,
}),
).toThrow(/Expected date, received function/)
})
})
10 changes: 10 additions & 0 deletions packages/core/lib/utils/toDateProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const toDatePreprocessor = (value: unknown) => {
switch (typeof value) {
case 'string':
case 'number':
return new Date(value)

default:
return value // could not coerce, return the original and face the consequences during validation
}
}
6 changes: 3 additions & 3 deletions packages/core/test/testContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { z } from 'zod'

import { DomainEventEmitter } from '../lib/events/DomainEventEmitter'
import { EventRegistry } from '../lib/events/EventRegistry'
import { EXTENDED_MESSAGE_SCHEMA } from '../lib/messages/baseMessageSchemas'
import { BASE_MESSAGE_SCHEMA } from '../lib/messages/baseMessageSchemas'
import type { TransactionObservabilityManager } from '../lib/types/MessageQueueTypes'

export const SINGLETON_CONFIG = { lifetime: Lifetime.SINGLETON }
Expand All @@ -19,7 +19,7 @@ const TestLogger: Logger = pino()

export const TestEvents = {
created: {
schema: EXTENDED_MESSAGE_SCHEMA.extend({
schema: BASE_MESSAGE_SCHEMA.extend({
type: z.literal('entity.created'),
payload: z.object({
message: z.string(),
Expand All @@ -28,7 +28,7 @@ export const TestEvents = {
},

updated: {
schema: EXTENDED_MESSAGE_SCHEMA.extend({
schema: BASE_MESSAGE_SCHEMA.extend({
type: z.literal('entity.updated'),
payload: z.object({
message: z.string(),
Expand Down

0 comments on commit 5d9c2a7

Please sign in to comment.