mirror of
https://github.com/TesseraKit/Core.git
synced 2026-11-01 20:41:20 +01:00
No description
- TypeScript 98%
- JavaScript 2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| src | ||
| test | ||
| .gitignore | ||
| bun.lock | ||
| bun.lockb | ||
| bunfig.toml | ||
| eslint.config.mjs | ||
| LICENSE | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsconfig.test.json | ||
| tsup.config.ts | ||
| vitest.config.ts | ||
@tesserakit/core
Core primitives for building entity-driven applications with TesseraKit — a full-stack Bun + Effect-TS framework.
Installation
bun add @tesserakit/core
Modules
The package is tree-shakeable. Prefer sub-path imports:
| Import | Contents |
|---|---|
@tesserakit/core/id |
ULID and URN utilities |
@tesserakit/core/schema |
Entity, CRUD, and pagination schemas |
@tesserakit/core/errors |
Domain error classes |
@tesserakit/core/id
ULID — sortable, time-encoded unique IDs
import { Ulid } from "@tesserakit/core/id";
// Generate
const id = Ulid.generate(); // "01ARZ3NDEKTSV4RRFFQ69G5FAV"
// Validate
Ulid.isValid(id); // true
// Extract time
Ulid.timestamp(id); // Unix ms
Ulid.date(id); // Date object
// Sort-safe comparison
[id3, id1, id2].sort(Ulid.compare); // chronological order
// Guaranteed ordering within same millisecond
const gen = Ulid.monotonic();
const [a, b, c] = [gen(), gen(), gen()]; // a < b < c always
// Effect-based API
import { Effect } from "effect";
const id2 = yield* Ulid.generateEffect();
const parsed = yield* Ulid.parseEffect("01ARZ3NDEKTSV4RRFFQ69G5FAV");
URN — namespaced entity identifiers
Format: {app}:{entity}:{ulid} — e.g. myapp:person:01ARZ3NDEKTSV4RRFFQ69G5FAV
import { Urn } from "@tesserakit/core/id";
// Create
const id = Urn.create("myapp", "person");
// Parse
const { app, entity, id: ulid } = Urn.parse(id);
// Validate
Urn.isValid(id); // true
// Type guards
Urn.isApp(id, "myapp"); // true
Urn.isEntity(id, "person"); // true
Urn.matches(id, "myapp", "person"); // true
// Scoped generator
const myappUrn = Urn.forApp("myapp");
const personId = myappUrn("person");
const productId = myappUrn("product");
// Effect-based API
const safeId = yield* Urn.createEffect("myapp", "person");
const components = yield* Urn.parseEffect(id);
const opt = Urn.parseSafe(id); // Option.Option<UrnComponents>
@tesserakit/core/schema
Entity schemas
import { createEntitySchema, createCrudSchemas } from "@tesserakit/core/schema";
import { Schema as S } from "effect";
// 1. Define the entity (extends BaseEntity automatically)
const PersonSchema = createEntitySchema("Person", {
firstName: S.String.pipe(S.minLength(1)),
lastName: S.String.pipe(S.minLength(1)),
email: S.String.pipe(S.minLength(1)),
bio: S.optional(S.String),
}, {
softDelete: true, // adds deletedAt / deletedBy / isDeleted
description: "A person in the system",
});
export type Person = S.Schema.Type<typeof PersonSchema>;
// 2. Generate CRUD schemas (no boilerplate)
export const PersonCrud = createCrudSchemas(PersonSchema, "Person");
// Produces: CreateInput, UpdateInput, FindByIdInput, FindManyInput, DeleteInput, PaginatedResponse
BaseEntity fields
Every entity automatically includes:
| Field | Type | Description |
|---|---|---|
id |
UrnType |
Auto-generated URN identifier |
createdAt |
Date |
Creation timestamp |
updatedAt |
Date |
Last-updated timestamp |
createdBy |
UrnType? |
Actor who created the entity |
updatedBy |
UrnType? |
Actor who last updated the entity |
With softDelete: true, also adds: deletedAt, deletedBy, isDeleted.
Pagination
import { PaginationParams, createPaginatedResponse, normalizePaginationParams } from "@tesserakit/core/schema";
// Normalize with defaults
const params = normalizePaginationParams({ limit: 200 }); // clamped to MAX_LIMIT
// Build a paginated response
const response = createPaginatedResponse(items, totalCount, params);
// { data: [...], meta: { total, limit, offset, hasMore, totalPages, currentPage } }
Query filters
FindManyInput supports structured filters:
const query = {
filters: [
{ field: "firstName", operator: "eq", value: "Alice" },
{ field: "createdAt", operator: "gte", value: "2024-01-01T00:00:00Z" },
{ field: "bio", operator: "isNotNull", value: null },
],
pagination: { limit: 20, offset: 0, sortBy: "createdAt", sortDirection: "desc" },
};
Supported operators: eq neq gt gte lt lte in nin contains startsWith endsWith isNull isNotNull
@tesserakit/core/errors
All errors extend Data.TaggedError — they are first-class Effect values.
import {
EntityNotFoundError,
EntityConflictError,
EntityValidationError,
RepositoryError,
UnauthorizedError,
ForbiddenError,
} from "@tesserakit/core/errors";
import { Effect } from "effect";
// Catch with Effect.catchTag
const person = yield* personRepo.findById(id).pipe(
Effect.catchTag("EntityNotFoundError", () => Effect.succeed(null)),
);
// Catch multiple
yield* personRepo.create(input).pipe(
Effect.catchTags({
EntityConflictError: (e) => Effect.fail(new ApiError({ message: `${e.field} already exists` })),
EntityValidationError: (e) => Effect.fail(new ApiError({ message: e.message })),
RepositoryError: (e) => { console.error(e.cause); return Effect.fail(e); },
}),
);
| Error | Tag | Use when |
|---|---|---|
EntityNotFoundError |
"EntityNotFoundError" |
findById / update / delete misses |
EntityConflictError |
"EntityConflictError" |
unique constraint violations |
EntityValidationError |
"EntityValidationError" |
business rule violations |
RepositoryError |
"RepositoryError" |
persistence layer failures |
UnauthorizedError |
"UnauthorizedError" |
missing / invalid credentials |
ForbiddenError |
"ForbiddenError" |
authenticated but not permitted |
Design principles
- Effect-first: every async or fallible operation has an
Effect-based variant (createEffect,parseEffect, etc.) - URN IDs: entity IDs encode app namespace + entity type + time-sortable ULID — enabling efficient range queries and multi-app deployments
- Schema-first: define your entity once; CRUD schemas, pagination, and filters are all derived
- Transport-agnostic errors:
@tesserakit/core/errorshas no knowledge of HTTP status codes or RPC error codes — that mapping lives in@tesserakit/server
License
GPL-3.0-only