Skip to content

Commit

Permalink
Add message-queue-toolkit/schemas (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
kibertoad committed May 23, 2024
1 parent b188b2d commit e5cf0e3
Show file tree
Hide file tree
Showing 18 changed files with 459 additions and 231 deletions.
81 changes: 12 additions & 69 deletions packages/core/lib/events/baseEventSchemas.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,12 @@
import type { ZodLiteral, ZodObject, ZodOptional, ZodString } from 'zod'
import z from 'zod'
import type { ZodRawShape } from 'zod/lib/types'

// Base event fields that are typically autogenerated
export const GENERATED_BASE_EVENT_SCHEMA = z.object({
id: z.string().describe('event unique identifier'),
timestamp: z.string().datetime().describe('iso 8601 datetime'),
})

// Base event fields that are typically autogenerated, marked as optional
export const OPTIONAL_GENERATED_BASE_EVENT_SCHEMA = z.object({
id: z.string().describe('event unique identifier').optional(),
timestamp: z.string().datetime().describe('iso 8601 datetime').optional(),
})

// Base event fields that are always defined manually
export const CORE_EVENT_SCHEMA = z.object({
type: z.literal<string>('<replace.me>').describe('event type name'),
payload: z.optional(z.object({})).describe('event payload based on type'),
})

// Core fields that describe either internal event or external message
export const CONSUMER_BASE_EVENT_SCHEMA = GENERATED_BASE_EVENT_SCHEMA.extend(
CORE_EVENT_SCHEMA.shape,
)
export const PUBLISHER_BASE_EVENT_SCHEMA = OPTIONAL_GENERATED_BASE_EVENT_SCHEMA.extend(
CORE_EVENT_SCHEMA.shape,
)

export type ConsumerBaseEventType = z.infer<typeof CONSUMER_BASE_EVENT_SCHEMA>
export type PublisherBaseEventType = z.infer<typeof PUBLISHER_BASE_EVENT_SCHEMA>
export type CoreEventType = z.infer<typeof CORE_EVENT_SCHEMA>
export type GeneratedBaseEventType = z.infer<typeof GENERATED_BASE_EVENT_SCHEMA>

type ReturnType<T extends ZodObject<Y>, Y extends ZodRawShape, Z extends string> = {
consumerSchema: ZodObject<{
id: ZodString
timestamp: ZodString
type: ZodLiteral<Z>
payload: T
}>

publisherSchema: ZodObject<{
id: ZodOptional<ZodString>
timestamp: ZodOptional<ZodString>
type: ZodLiteral<Z>
payload: T
}>
}

export function enrichEventSchemaWithBase<
T extends ZodObject<Y>,
Y extends ZodRawShape,
Z extends string,
>(type: Z, payloadSchema: T): ReturnType<T, Y, Z> {
const baseSchema = z.object({
type: z.literal(type),
payload: payloadSchema,
})

const consumerSchema = GENERATED_BASE_EVENT_SCHEMA.merge(baseSchema)
const publisherSchema = OPTIONAL_GENERATED_BASE_EVENT_SCHEMA.merge(baseSchema)

return {
consumerSchema: consumerSchema,
publisherSchema: publisherSchema,
}
}
export {
GENERATED_BASE_EVENT_SCHEMA,
OPTIONAL_GENERATED_BASE_EVENT_SCHEMA,
CORE_EVENT_SCHEMA,
CONSUMER_BASE_EVENT_SCHEMA,
PUBLISHER_BASE_EVENT_SCHEMA,
ConsumerBaseEventType,
PublisherBaseEventType,
CoreEventType,
GeneratedBaseEventType,
enrichEventSchemaWithBase,
} from '@message-queue-toolkit/schemas'
64 changes: 9 additions & 55 deletions packages/core/lib/events/eventTypes.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,9 @@
import type { ZodObject, ZodTypeAny } from 'zod'
import type z from 'zod'

import type { MessageMetadataType } from '../messages/baseMessageSchemas'

import type { CONSUMER_BASE_EVENT_SCHEMA, PUBLISHER_BASE_EVENT_SCHEMA } from './baseEventSchemas'

export type EventTypeNames<EventDefinition extends CommonEventDefinition> =
CommonEventDefinitionConsumerSchemaType<EventDefinition>['type']

export type CommonEventDefinition = {
consumerSchema: ZodObject<
Omit<(typeof CONSUMER_BASE_EVENT_SCHEMA)['shape'], 'payload'> & { payload: ZodTypeAny }
>
publisherSchema: ZodObject<
Omit<(typeof PUBLISHER_BASE_EVENT_SCHEMA)['shape'], 'payload'> & { payload: ZodTypeAny }
>
schemaVersion?: string
}

export type CommonEventDefinitionConsumerSchemaType<T extends CommonEventDefinition> = z.infer<
T['consumerSchema']
>

export type CommonEventDefinitionPublisherSchemaType<T extends CommonEventDefinition> = z.infer<
T['publisherSchema']
>

export type EventHandler<
EventDefinitionSchema extends
CommonEventDefinitionConsumerSchemaType<CommonEventDefinition> = CommonEventDefinitionConsumerSchemaType<CommonEventDefinition>,
MetadataDefinitionSchema extends Partial<MessageMetadataType> = Partial<MessageMetadataType>,
> = {
handleEvent(
event: EventDefinitionSchema,
metadata?: MetadataDefinitionSchema,
): void | Promise<void>
}

export type AnyEventHandler<EventDefinitions extends CommonEventDefinition[]> = EventHandler<
CommonEventDefinitionConsumerSchemaType<EventDefinitions[number]>
>

export type SingleEventHandler<
EventDefinition extends CommonEventDefinition[],
EventTypeName extends EventTypeNames<EventDefinition[number]>,
> = EventHandler<EventFromArrayByTypeName<EventDefinition, EventTypeName>>

type EventFromArrayByTypeName<
EventDefinition extends CommonEventDefinition[],
EventTypeName extends EventTypeNames<EventDefinition[number]>,
> = Extract<
CommonEventDefinitionConsumerSchemaType<EventDefinition[number]>,
{ type: EventTypeName }
>
export {
EventTypeNames,
CommonEventDefinition,
CommonEventDefinitionConsumerSchemaType,
CommonEventDefinitionPublisherSchemaType,
EventHandler,
AnyEventHandler,
SingleEventHandler,
} from '@message-queue-toolkit/schemas'
102 changes: 9 additions & 93 deletions packages/core/lib/messages/baseMessageSchemas.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,9 @@
import z, { type ZodLiteral, type ZodObject, type ZodOptional, type ZodString } from 'zod'
import type { ZodRawShape } from 'zod/lib/types'

import {
CONSUMER_BASE_EVENT_SCHEMA,
GENERATED_BASE_EVENT_SCHEMA,
OPTIONAL_GENERATED_BASE_EVENT_SCHEMA,
} from '../events/baseEventSchemas'
import type { CommonEventDefinition } from '../events/eventTypes'

// 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')

export const MESSAGE_SCHEMA_EXTENSION = {
// 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 const BASE_MESSAGE_SCHEMA = CONSUMER_BASE_EVENT_SCHEMA.extend(MESSAGE_SCHEMA_EXTENSION)

export type BaseMessageType = z.infer<typeof BASE_MESSAGE_SCHEMA>

export type MessageMetadataType = z.infer<typeof MESSAGE_METADATA_SCHEMA>

export type CommonMessageDefinitionSchemaType<T extends CommonEventDefinition> = z.infer<
T['consumerSchema']
>

type ReturnType<T extends ZodObject<Y>, Y extends ZodRawShape, Z extends string> = {
consumerSchema: ZodObject<{
id: ZodString
timestamp: ZodString
type: ZodLiteral<Z>
payload: T
metadata: ZodOptional<
ZodObject<{
schemaVersion: ZodString
producedBy: ZodString
originatedFrom: ZodString
correlationId: ZodString
}>
>
}>

publisherSchema: ZodObject<{
id: ZodOptional<ZodString>
timestamp: ZodOptional<ZodString>
type: ZodLiteral<Z>
payload: T
metadata: ZodOptional<
ZodObject<{
schemaVersion: ZodString
producedBy: ZodString
originatedFrom: ZodString
correlationId: ZodString
}>
>
}>
}

export function enrichMessageSchemaWithBase<
T extends ZodObject<Y>,
Y extends ZodRawShape,
Z extends string,
>(type: Z, payloadSchema: T): ReturnType<T, Y, Z> {
const baseSchema = z.object({
type: z.literal(type),
payload: payloadSchema,
})

const consumerSchema =
GENERATED_BASE_EVENT_SCHEMA.merge(baseSchema).extend(MESSAGE_SCHEMA_EXTENSION)
const publisherSchema =
OPTIONAL_GENERATED_BASE_EVENT_SCHEMA.merge(baseSchema).extend(MESSAGE_SCHEMA_EXTENSION)

return {
consumerSchema: consumerSchema,
publisherSchema: publisherSchema,
}
}
export {
MESSAGE_METADATA_SCHEMA,
MESSAGE_SCHEMA_EXTENSION,
BASE_MESSAGE_SCHEMA,
BaseMessageType,
MessageMetadataType,
CommonMessageDefinitionSchemaType,
enrichMessageSchemaWithBase,
} from '@message-queue-toolkit/schemas'
11 changes: 1 addition & 10 deletions packages/core/lib/utils/toDateProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1 @@
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
}
}
export { toDatePreprocessor } from '@message-queue-toolkit/schemas'
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
},
"dependencies": {
"@lokalise/node-core": "^9.17.0",
"@message-queue-toolkit/schemas": "^1.0.0",
"fast-equals": "^5.0.1",
"toad-cache": "^3.7.0",
"zod": "^3.23.8"
Expand Down
8 changes: 4 additions & 4 deletions packages/core/vitest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ export default defineConfig({
reporter: ['text'],
all: true,
thresholds: {
lines: 35,
functions: 65,
branches: 80,
statements: 35,
lines: 30,
functions: 60,
branches: 75,
statements: 30,
},
},
},
Expand Down
4 changes: 4 additions & 0 deletions packages/schemas/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
coverage/
dist/
vitest.config.mts
80 changes: 80 additions & 0 deletions packages/schemas/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module",
"project": "./tsconfig.json"
},
"ignorePatterns": ["node_modules"],
"plugins": ["@typescript-eslint", "vitest", "import"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:vitest/recommended"
],
"rules": {
"@typescript-eslint/no-empty-interface": "warn",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/indent": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/consistent-type-imports": "warn",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
],
"@typescript-eslint/member-delimiter-style": [
"error",
{
"multiline": {
"delimiter": "none",
"requireLast": false
},
"singleline": {
"delimiter": "comma",
"requireLast": false
}
}
],
"import/no-default-export": "error",
"import/order": [
"warn",
{
"alphabetize": { "order": "asc" },
"newlines-between": "always"
}
],
"max-lines": ["error", { "max": 600 }],
"max-params": ["error", { "max": 4 }],
"max-statements": ["error", { "max": 15 }],
"complexity": ["error", { "max": 20 }]
},
"overrides": [
{
"files": ["test/**/*.ts", "*.test.ts", "*.spec.ts"],
"rules": {
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-call": "off",
"max-statements": ["off"]
}
}
]
}
9 changes: 9 additions & 0 deletions packages/schemas/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"printWidth": 100,
"singleQuote": true,
"bracketSpacing": true,
"semi": false,
"arrowParens": "always",
"endOfLine": "lf",
"trailingComma": "all"
}
5 changes: 5 additions & 0 deletions packages/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { toDatePreprocessor } from './lib/utils/toDateProcessor'

export * from './lib/events/eventTypes'
export * from './lib/events/baseEventSchemas'
export * from './lib/messages/baseMessageSchemas'
Loading

0 comments on commit e5cf0e3

Please sign in to comment.