Skip to content

Releases: elysiajs/elysia

1.1.20

10 Oct 14:10
fad6445
Compare
Choose a tag to compare

What's new

Bug fix:

  • merge guard and not specified hook responses status

Full Changelog: 1.1.19...1.1.20

1.1.19

07 Oct 10:31
93d457b
Compare
Choose a tag to compare

What's new

Bug fix:

  • unable to return error from derive/resolve

Full Changelog: 1.1.18...1.1.19

1.1.18

04 Oct 16:13
37ba784
Compare
Choose a tag to compare

What's new

Breaking change:

  • remove automatic conversion of 1-level deep object with file field to formdata
    • migration: wrap a response with formdata
  • (internal): remove ELYSIA_RESPONSE symbol
  • (internal) error now use class ElysiaCustomStatusResponse instead of plain object

Improvement:

  • Optimize object type response mapping performance

Full Changelog: 1.1.17...1.1.18

1.1.17

29 Sep 11:19
9da8228
Compare
Choose a tag to compare

What's new

Change:

  • Coerce number to numeric on body root automatically
  • Coerce boolean to booleanString on body root automatically

Bug fix:

  • #838 invalid onAfterResponse typing
  • #855 Validation with Numeric & Number options doesn't work
  • #843 Resolve does not work with aot: false

Full Changelog: 1.1.16...1.1.17

1.1.16

23 Sep 13:09
6c1c1da
Compare
Choose a tag to compare

What's new

Bug fix:

  • separate between createStaticHandler and createNativeStaticHandler for maintainability
  • performance degradation using inline fetch on text static response and file

Full Changelog: 1.1.15...1.1.16

1.1.15

23 Sep 05:47
92ca044
Compare
Choose a tag to compare

What's new

Bug fix:

  • createStaticResponse unintentionally mutate set.headers

Full Changelog: 1.1.14...1.1.15

1.1.14

23 Sep 05:35
7afb00a
Compare
Choose a tag to compare

What's Changed

Feature:

  • add auto-completion to Content-Type headers

Bug fix:

  • exclude file from Bun native static response until Bun support
  • set 'text/plain' for string if no content-type is set for native static response

Full Changelog: 1.1.13...1.1.14

1.1.13

18 Sep 07:57
aadb7c6
Compare
Choose a tag to compare

What's Changed

Feature:

Bug fix:

  • #830 Incorrect type for ws.publish
  • #827 returning a response is forcing application/json content-type
  • #821 handle "+" in query with validation
  • #820 params in hooks inside prefixed groups are incorrectly typed never
  • #819 setting cookie attribute before value cause cookie attribute to not be set
  • #810 wrong inference of response in afterResponse, includes status code

New Contributors

Full Changelog: 1.1.12...1.1.13

1.1.12

05 Sep 09:42
cb56831
Compare
Choose a tag to compare

Feature:

  • setup provenance publish
  • #808 add UnionEnum type with JSON schema enum usage
  • #807 add closeActiveConnections to Elysia.stop()

Bug fix:

  • #808 ArrayString type cast as Object instead of Array
  • config.nativeStaticResponse is not defined

1.1 - Grown-up's Paradise

16 Jul 16:22
86613e7
Compare
Choose a tag to compare

elysia-11

Named after a song by Mili, "Grown-up's Paradise", and used as opening for commercial announcement of Arknights TV animation season 3.

As a day one Arknights player and long time Mili's fan, never once I would thought Mili would do a song for Arknights, you should check them out as they are the goat.

Elysia 1.1 focus on several improvement to Developer Experience as follows:

OpenTelemetry

Observability is one of an important aspect for production.

It allows us to understand how our server works on production, identifying problems and bottlenecks.

One of the most popular tools for observability is OpenTelemetry. However, we acknowledge that it's hard and take time to setup and instrument your server correctly.

It's hard to integrate OpenTelemetry to most existing framework and library.

Most revolve around hacky solution, monkey patching, prototype pollution, or manual instrumentation as the framework is not designed for observability from the start.

That's why we introduce first party support for OpenTelemetry on Elysia

To start using OpenTelemetry, install @elysiajs/opentelemetry and apply plugin to any instance.

import { Elysia } from 'elysia'
import { opentelemetry } from '@elysiajs/opentelemetry'

import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'

new Elysia()
	.use(
		opentelemetry({
			spanProcessors: [
				new BatchSpanProcessor(
					new OTLPTraceExporter()
				)
			]
		})
	)

jaeger showing collected trace automatically

Elysia OpenTelemetry is will collect span of any library compatible OpenTelemetry standard, and will apply parent and child span automatically.

In the code above, we apply Prisma to trace how long each query took.

By applying OpenTelemetry, Elysia will then:

  • collect telemetry data
  • Grouping relevant lifecycle together
  • Measure how long each function took
  • Instrument HTTP request and response
  • Collect error and exception

You can export telemetry data to Jaeger, Zipkin, New Relic, Axiom or any other OpenTelemetry compatible backend.

Here's an example of exporting telemetry to Axiom

const Bun = {
	env: {
		AXIOM_TOKEN: '',
		AXIOM_DATASET: ''
	}
}
// ---cut---
import { Elysia } from 'elysia'
import { opentelemetry } from '@elysiajs/opentelemetry'

import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'

new Elysia()
	.use(
		opentelemetry({
			spanProcessors: [
				new BatchSpanProcessor(
					new OTLPTraceExporter({
						url: 'https://api.axiom.co/v1/traces', // [!code ++]
						headers: { // [!code ++]
						    Authorization: `Bearer ${Bun.env.AXIOM_TOKEN}`, // [!code ++]
						    'X-Axiom-Dataset': Bun.env.AXIOM_DATASET // [!code ++]
						} // [!code ++]
					})
				)
			]
		})
	)

axiom showing collected trace from OpenTelemetry

Elysia OpenTelemetry is for applying OpenTelemetry to Elysia server only.

You can use OpenTelemetry SDK normally, and the span is run under Elysia's request span, it will be automatically appear in Elysia trace.

However, we also provide a getTracer, and record utility to collect span from any part of your application.

const db = {
	query(query: string) {
		return new Promise<unknown>((resolve) => {
			resolve('')
		})
	}
}
// ---cut---
import { Elysia } from 'elysia'
import { record } from '@elysiajs/opentelemetry'

export const plugin = new Elysia()
	.get('', () => {
		return record('database.query', () => {
			return db.query('SELECT * FROM users')
		})
	})

record is an equivalent to OpenTelemetry's startActiveSpan but it will handle auto-closing and capture exception automatically.

You may think of record as a label for your code that will be shown in trace.

Prepare your codebase for observability

Elysia OpenTelemetry will group lifecycle and read the function name of each hook as the name of the span.

It's a good time to name your function.

If your hook handler is an arrow function, you may refactor it to named function to understand the trace better otherwise, your trace span will be named as anonymous.

const bad = new Elysia()
	// ⚠️ span name will be anonymous
	.derive(async ({ cookie: { session } }) => {
		return {
			user: await getProfile(session)
		}
	})

const good = new Elysia()
	// ✅ span name will be getProfile
	.derive(async function getProfile({ cookie: { session } }) {
		return {
			user: await getProfile(session)
		}
	})

Trace v2

Elysia OpenTelemetry is built on Trace v2, replacing Trace v1.

Trace v2 allows us to trace any part of our server with 100% synchronous behavior, instead of relying on parallel event listener bridge (goodbye dead lock)

It's entirely rewritten to not only be faster, but also reliable, and accurate down to microsecond by relying on Elysia's ahead of time compilation and code injection.

Trace v2 use a callback listener instead of Promise to ensure that callback is finished before moving on to the next lifecycle event.

Here's an example usage of Trace v2:

import { Elysia } from 'elysia'

new Elysia()
	.trace(({ onBeforeHandle, set }) => {
		// Listen to before handle event
		onBeforeHandle(({ onEvent }) => {
			// Listen to all child event in order
			onEvent(({ onStop, name }) => {
				// Execute something after a child event is finished
				onStop(({ elapsed }) => {
					console.log(name, 'took', elapsed, 'ms')

					// callback is executed synchronously before next event
					set.headers['x-trace'] = 'true'
				})
			})
		})
	})

You may also use async inside trace, Elysia will block and event before proceeding to the next event until the callback is finished.

Trace v2 is a breaking change to Trace v1, please check out trace api documentation for more information.

Normalization

Elysia 1.1 now normalize data before it's being processed.

To ensure that data is consistent and safe, Elysia will try to coerce data into an exact data shape defined in schema, removing additional fields, and normalizing data into a consistent format.

For example if you have a schema like this:

// @errors: 2353
import { Elysia, t } from 'elysia'
import { treaty } from '@elysiajs/eden'

const app = new Elysia()
	.post('/', ({ body }) => body, {
		body: t.Object({
			name: t.String(),
			point: t.Number()
		}),
		response: t.Object({
			name: t.String()
		})
	})

const { data } = await treaty(app).index.post({
	name: 'SaltyAom',
	point: 9001,
	// ⚠️ additional field
	title: 'maintainer'
})

// 'point' is removed as defined in response
console.log(data) // { name: 'SaltyAom' }

This code does 2 thing:

  • Remove title from body before it's being used on the server
  • Remove point from response before it's being sent to the client

This is useful to prevent data inconsistency, and ensure that data is always in the correct format, and not leaking any sensitive information.

Data type coercion

Previously Elysia is using an exact data type without coercion unless explicitly specified to.

For example, to parse a query parameter as a number, you need to explicitly cast it as t.Numeric instead of t.Number.

import { Elysia, t } from 'elysia'

const app = new Elysia()
	.get('/', ({ query }) => query, {
		query: t.Object({
			page: t.Numeric()
		})
	})

However, in Elysia 1.1, we introduce data type coercion, which will automatically coerce data into the correct data type if possible.

Allowing us to simply set t.Number instead of t.Numeric to parse a query parameter as a number.

import { Elysia, t } from 'elysia'

const app = new Elysia()
	.get('/', ({ query }) => query, {
		query: t.Object({
			// ✅ page will be coerced into a number automatically
			page: t.Number()
		})
	})

This also apply to t.Boolean, t.Object, and t.Array.

This is done by swapping schema with possible coercion counterpart during compilation phase ahead of time, and has the same as using t.Numeric or other coercion counterpart.

Guard as

Previously, guard will only apply to the current instance only.

import { Elysia } from 'elysia'

const plugin = new Elysia()
	.guard({
		beforeHandle() {
			console.log('called')
		}
	})
	.get('/plugin', () => 'ok')

const main = new Elysia()
	.use(plugin)
	.get('/', () => 'ok')

Using this code, onBeforeHandle will only be called when accessing /plugin but not /.

In Elysia 1.1, we add as property to guard allowing us to apply guard as scoped or global as same as adding event listener.

import { Elysia } from 'elysia'

const plugin1 = new Elysia()
	.guard({
		as: 'scoped', // [!code ++]
		beforeHandle() {
			console.log('called')
		}
	})
	.get('/plugin',...
Read more