Does anyone have any tips on implementing type saf...
# general
j
Does anyone have any tips on implementing type safety for EventBridge Lambda targets? Both for the handler and when pushing events in. Atm, I'm just writing it out as a type we can import, but it's sort of a duplicate of the rules setup in the SST side.
t
We're working on this!
I can share the beginnings of what I have which solve half the problem
j
Ooo neat! Yea, sure, it'll be nice to get some ideas whilst waiting as well.
t
ok here's what I've got, pretty rough at the moment
I have a little module with this (ignore the dataloader and actor context part that's a bit specific to my codebase)
Copy code
const client = new EventBridgeClient({})

async function doPublish(events: readonly Event[]) {
  await client.send(
    new PutEventsCommand({
      Entries: events.map((item) => ({
        EventBusName: Config.BUS_NAME,
        Detail: JSON.stringify(item),
        Source: item.source,
        DetailType: item.type,
      })),
    })
  )

  return events
}

export interface Event {
  id: string
  source: "bumi"
  type: string
  properties: any
  actor: Actor
}

export interface Events {}

export type EventTypes = keyof Events

export type FromType<T extends EventTypes> = payload<T, Events[T]>

export type AnyEvent = FromType<EventTypes>

export type payload<T extends string, P extends Record<string, any>> = {
  id: string
  source: "bumi"
  type: T
  properties: P
  actor: Actor
}

export async function publish<T extends EventTypes>(
  type: T,
  properties: Events[T],
  actor?: Actor
) {
  const ctxActor = useContext(ActorContext)
  const payload: Event = {
    id: ulid(),
    source: "bumi",
    type,
    properties,
    actor: actor ? actor : ctxActor,
  }
  await useLoader(publish, doPublish).load(payload)
  return payload
}
then across my codebase I can define events like this:
Copy code
declare module "@bumi/core/bus" {
  export interface Events {
    "business.created": {
      id: string
      name: string
    }
    "business.deleted": {
      id: string
    }
  }
}
this makes
Bus.publish(...)
typesafe
I also have this function to create a handle (I route all my eventbridge subscriptions through an SQS queue)
Copy code
export function createHandler<T extends EventTypes>(
  cb: (event: FromType<T>, record: SQSRecord) => Promise<void>
) {
  const result = async (event: SQSEvent) => {
    const promises = []
    for (const record of event.Records) {
      const msg = JSON.parse(record.body)
      async function run() {
        try {
          await cb(msg.detail as FromType<T>, record)
          return { type: "success" }
        } catch (e) {
          console.error(e)
          return { type: "error", messageId: record.messageId }
        }
      }
      promises.push(run())
    }
    const results = await Promise.all(promises)
    return {
      batchItemFailures: results
        .filter((i) => i.type === "error")
        .map((i) => ({
          itemIdentifier: i,
        })),
    }
  }
  return result
  // return AWSLambda.wrapHandler(result)
}
can use it like this
Copy code
export const handler = createHandler<"business.created">(async (evt) => {
there is still duplication but we're thinking about publishing a lot of this as a library you can use and then having the
sst.EventBus
construct scan your code for
createHandler
and automatically setup subscriptions for you
j
@justindra please stop reading our mind on what we are working on, thank you
j
Oh neat, ok so kinda similar to what I've got atm, but I do like the type augmentation that you are doing. That would make it a lot less type importing going around in the code. And yea, that would be an interesting one around scanning the code to generate constructs.
Hahaha, well.. while on reading minds and that you mentioned code scanning. It would be amazing to generate automatic API and EventBridge docs too 😛