@roastery/terroir 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alan Reis Anjos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,226 @@
1
+ # @roastery/terroir
2
+
3
+ Layered exception hierarchy and runtime schema validation for the [Roastery CMS](https://github.com/roastery-cms) ecosystem.
4
+
5
+ [![Checked with Biome](https://img.shields.io/badge/Checked_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev)
6
+
7
+ ## Overview
8
+
9
+ **terroir** provides two core primitives for building robust, type-safe TypeScript applications:
10
+
11
+ - **Exception hierarchy** — A structured, symbol-tagged exception system organized by architectural layer (Domain, Application, Infrastructure), designed for Clean Architecture and DDD applications.
12
+ - **Schema validation** — A runtime validation and coercion engine built on [TypeBox](https://github.com/sinclairzx81/typebox), with support for custom string formats like UUID v7, slug, email, and more.
13
+
14
+ ## Technologies
15
+
16
+ | Tool | Purpose |
17
+ |------|---------|
18
+ | [TypeBox](https://github.com/sinclairzx81/typebox) | Runtime schema validation and TypeScript type inference |
19
+ | [uuid](https://github.com/uuidjs/uuid) | UUID v7 format validation |
20
+ | [tsup](https://tsup.egoist.dev) | Bundling to ESM + CJS with `.d.ts` generation |
21
+ | [Bun](https://bun.sh) | Runtime, test runner, and package manager |
22
+ | [Knip](https://knip.dev) | Unused exports and dependency detection |
23
+ | [Husky](https://typicode.github.io/husky) + [commitlint](https://commitlint.js.org) | Git hooks and conventional commit enforcement |
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ bun add @roastery/terroir
29
+ ```
30
+
31
+ **Peer dependencies** (install alongside):
32
+
33
+ ```bash
34
+ bun add @sinclair/typebox uuid
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Exceptions
40
+
41
+ All exceptions extend `CoreException` and carry a Symbol-tagged `[ExceptionLayer]` property for runtime layer detection.
42
+
43
+ ```typescript
44
+ import { ExceptionLayer } from '@roastery/terroir/exceptions/symbols';
45
+
46
+ function handleError(err: unknown) {
47
+ if (err instanceof Error && ExceptionLayer in err) {
48
+ const layer = (err as any)[ExceptionLayer]; // 'application' | 'domain' | 'infra' | 'internal'
49
+ console.log(`Error from layer: ${layer}`);
50
+ }
51
+ }
52
+ ```
53
+
54
+ ### Application layer
55
+
56
+ Errors related to business logic, request handling, and user input.
57
+
58
+ ```typescript
59
+ import { BadRequestException } from '@roastery/terroir/exceptions/application';
60
+
61
+ throw new BadRequestException('Invalid input', 'UserController');
62
+ ```
63
+
64
+ | Class | When to use |
65
+ |-------|-------------|
66
+ | `BadRequestException` | Invalid or malformed input |
67
+ | `UnauthorizedException` | Authentication required or failed |
68
+ | `InvalidOperationException` | Operation not allowed in current state |
69
+ | `ResourceNotFoundException` | Requested resource does not exist |
70
+ | `ResourceAlreadyExistsException` | Duplicate resource creation attempt |
71
+ | `InvalidJwtException` | JWT token is invalid |
72
+ | `UnableToSignPayloadException` | JWT signing failed |
73
+
74
+ ### Domain layer
75
+
76
+ Errors from domain model constraint violations.
77
+
78
+ ```typescript
79
+ import { InvalidPropertyException } from '@roastery/terroir/exceptions/domain';
80
+
81
+ throw new InvalidPropertyException('email', 'UserEntity');
82
+ ```
83
+
84
+ | Class | When to use |
85
+ |-------|-------------|
86
+ | `InvalidDomainDataException` | Domain invariant violated |
87
+ | `InvalidPropertyException` | Entity property failed validation |
88
+ | `OperationFailedException` | Domain operation could not complete |
89
+
90
+ ### Infrastructure layer
91
+
92
+ Errors from external services and I/O operations.
93
+
94
+ ```typescript
95
+ import { DatabaseUnavailableException } from '@roastery/terroir/exceptions/infra';
96
+
97
+ throw new DatabaseUnavailableException('PostgresRepository');
98
+ ```
99
+
100
+ | Class | When to use |
101
+ |-------|-------------|
102
+ | `DatabaseUnavailableException` | Database connection failed |
103
+ | `CacheUnavailableException` | Cache service unreachable |
104
+ | `UnexpectedCacheValueException` | Cache returned unexpected data |
105
+ | `ConflictException` | Unique constraint violation |
106
+ | `ForeignDependencyConstraintException` | Foreign key constraint violation |
107
+ | `ResourceNotFoundException` | Record not found in data store |
108
+ | `OperationNotAllowedException` | Operation rejected by data store |
109
+ | `InvalidEnvironmentException` | Missing or invalid environment config |
110
+ | `MissingPluginDependencyException` | Required plugin not registered |
111
+
112
+ ### Internal exceptions
113
+
114
+ Rarely used directly — reserved for framework-level error handling.
115
+
116
+ ```typescript
117
+ import { UnknownException, InvalidEntityData, InvalidObjectValueException } from '@roastery/terroir/exceptions';
118
+ ```
119
+
120
+ ### Base classes and type utilities
121
+
122
+ ```typescript
123
+ // Extend these to create your own layer-specific exceptions
124
+ import { ApplicationException, DomainException, InfraException } from '@roastery/terroir/exceptions/models';
125
+
126
+ // Type utilities for mapping exception constructors by layer
127
+ import type { CaffeineExceptionKeysByLayer, CaffeineExceptionKeys, CaffeineExceptionRecords } from '@roastery/terroir/exceptions/types';
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Schema Validation
133
+
134
+ ```typescript
135
+ import { Schema } from '@roastery/terroir/schema';
136
+ import { Type } from '@sinclair/typebox';
137
+
138
+ // Importing the schema module also registers all custom formats
139
+ const UserSchema = new Schema(
140
+ Type.Object({
141
+ id: Type.String({ format: 'uuid' }),
142
+ email: Type.String({ format: 'email' }),
143
+ slug: Type.String({ format: 'slug' }),
144
+ createdAt: Type.String({ format: 'date-time' }),
145
+ })
146
+ );
147
+
148
+ // Validate input
149
+ if (UserSchema.match(data)) {
150
+ // data is typed as Static<typeof UserSchema>
151
+ }
152
+
153
+ // Coerce and clean input (removes extra properties, applies defaults)
154
+ const user = UserSchema.map(rawInput);
155
+
156
+ // Serialize schema to JSON string
157
+ const json = UserSchema.toString();
158
+ ```
159
+
160
+ ### Dynamic schema loading
161
+
162
+ Load and compile schemas at runtime from JSON strings (e.g., from a database or config file):
163
+
164
+ ```typescript
165
+ import { SchemaManager } from '@roastery/terroir/schema';
166
+ import type { TObject } from '@sinclair/typebox';
167
+
168
+ const schema = SchemaManager.build<TObject>('{"type":"object","properties":{...}}');
169
+
170
+ SchemaManager.isSchema(unknownValue); // boolean
171
+ ```
172
+
173
+ ### Available string formats
174
+
175
+ Automatically registered when importing `@roastery/terroir/schema`:
176
+
177
+ | Format | Description |
178
+ |--------|-------------|
179
+ | `uuid` | UUID v7 |
180
+ | `email` | Email address (RFC 5322) |
181
+ | `url` | Full URL with valid hostname |
182
+ | `simple-url` | Basic URL (no hostname requirement) |
183
+ | `slug` | URL slug (`kebab-case-only`) |
184
+ | `date-time` | ISO 8601 date-time string |
185
+ | `json` | Valid JSON string |
186
+
187
+ ---
188
+
189
+ ## Exports reference
190
+
191
+ ```typescript
192
+ import { ... } from '@roastery/terroir/exceptions'; // internal exceptions (rare)
193
+ import { ... } from '@roastery/terroir/exceptions/application'; // application layer
194
+ import { ... } from '@roastery/terroir/exceptions/application/jwt'; // JWT exceptions
195
+ import { ... } from '@roastery/terroir/exceptions/domain'; // domain layer
196
+ import { ... } from '@roastery/terroir/exceptions/infra'; // infra layer
197
+ import { ... } from '@roastery/terroir/exceptions/models'; // base classes
198
+ import { ... } from '@roastery/terroir/exceptions/symbols'; // ExceptionLayer symbol
199
+ import type { ... } from '@roastery/terroir/exceptions/types'; // type utilities
200
+ import { ... } from '@roastery/terroir/schema'; // Schema + SchemaManager
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Development
206
+
207
+ ```bash
208
+ # Run tests
209
+ bun run test:unit
210
+
211
+ # Run tests with coverage
212
+ bun run test:coverage
213
+
214
+ # Build for distribution
215
+ bun run build
216
+
217
+ # Check for unused exports and dependencies
218
+ bun run knip
219
+
220
+ # Full setup (knip + build + bun link)
221
+ bun run setup
222
+ ```
223
+
224
+ ## License
225
+
226
+ MIT
@@ -119,9 +119,7 @@ function hydrateSchema(schema) {
119
119
  const newSchema = { ...schema };
120
120
  if (newSchema.properties) {
121
121
  for (const key in newSchema.properties) {
122
- newSchema.properties[key] = hydrateSchema(
123
- newSchema.properties[key]
124
- );
122
+ newSchema.properties[key] = hydrateSchema(newSchema.properties[key]);
125
123
  }
126
124
  }
127
125
  if (newSchema.items) {
@@ -40,9 +40,7 @@ function hydrateSchema(schema) {
40
40
  const newSchema = { ...schema };
41
41
  if (newSchema.properties) {
42
42
  for (const key in newSchema.properties) {
43
- newSchema.properties[key] = hydrateSchema(
44
- newSchema.properties[key]
45
- );
43
+ newSchema.properties[key] = hydrateSchema(newSchema.properties[key]);
46
44
  }
47
45
  }
48
46
  if (newSchema.items) {
package/package.json CHANGED
@@ -1,58 +1,71 @@
1
1
  {
2
- "name": "@roastery/terroir",
3
- "version": "0.0.2",
4
- "type": "module",
5
- "author": {
6
- "name": "Alan Reis",
7
- "email": "alanreisanjo@gmail.com",
8
- "url": "https://hoyasumii.dev"
9
- },
10
- "main": "./dist/index.cjs",
11
- "module": "./dist/index.js",
12
- "types": "./dist/index.d.ts",
13
- "typesVersions": {
14
- "*": {
15
- "*": [
16
- "./dist/*/index.d.ts"
17
- ]
18
- }
19
- },
20
- "exports": {
21
- ".": {
22
- "types": "./dist/index.d.ts",
23
- "import": "./dist/index.js",
24
- "require": "./dist/index.cjs"
25
- },
26
- "./*": {
27
- "types": "./dist/*/index.d.ts",
28
- "import": "./dist/*/index.js",
29
- "require": "./dist/*/index.cjs"
30
- }
31
- },
32
- "files": [
33
- "dist"
34
- ],
35
- "scripts": {
36
- "build": "tsup 'src/**/index.ts' --format cjs,esm --dts --tsconfig tsconfig.build.json --clean",
37
- "test:unit": "bun test --env-file=.env.testing",
38
- "test:coverage": "bun test --env-file=.env.testing --coverage",
39
- "setup": "bun run build && bun link",
40
- "prepare": "husky || true"
41
- },
42
- "devDependencies": {
43
- "@faker-js/faker": "^10.2.0",
44
- "@vitest/coverage-v8": "^4.0.18",
45
- "@vitest/ui": "^4.0.18",
46
- "tsup": "^8.5.1",
47
- "@commitlint/cli": "^20.4.1",
48
- "@commitlint/config-conventional": "^20.4.1",
49
- "husky": "^9.1.7"
50
- },
51
- "peerDependencies": {
52
- "typescript": "^5",
53
- "@sinclair/typebox": ">= 0.34.0 < 1",
54
- "@types/bun": "latest",
55
- "uuid": "^13.0.0"
56
- },
57
- "dependencies": {}
2
+ "name": "@roastery/terroir",
3
+ "description": "Layered exception hierarchy and runtime schema validation for the Roastery CMS ecosystem",
4
+ "version": "0.0.4",
5
+ "type": "module",
6
+ "author": {
7
+ "name": "Alan Reis",
8
+ "email": "alanreisanjo@gmail.com",
9
+ "url": "https://github.com/Hoyasumii"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/roastery-cms/terroir"
14
+ },
15
+ "license": "MIT",
16
+ "main": "./dist/index.cjs",
17
+ "module": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "keywords": [
20
+ "hoyasumii",
21
+ "roastery",
22
+ "@roastery",
23
+ "terroir",
24
+ "@roastery/terroir",
25
+ "roastery-terroir"
26
+ ],
27
+ "typesVersions": {
28
+ "*": {
29
+ "*": [
30
+ "./dist/*/index.d.ts"
31
+ ]
32
+ }
33
+ },
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js",
38
+ "require": "./dist/index.cjs"
39
+ },
40
+ "./*": {
41
+ "types": "./dist/*/index.d.ts",
42
+ "import": "./dist/*/index.js",
43
+ "require": "./dist/*/index.cjs"
44
+ }
45
+ },
46
+ "files": [
47
+ "dist"
48
+ ],
49
+ "scripts": {
50
+ "build": "biome check --fix && tsup 'src/**/index.ts' --format cjs,esm --dts --tsconfig tsconfig.build.json --clean",
51
+ "test:unit": "bun test --env-file=.env.testing",
52
+ "test:coverage": "bun test --env-file=.env.testing --coverage",
53
+ "setup": "bun run build && bun link",
54
+ "prepare": "husky || true",
55
+ "knip": "knip"
56
+ },
57
+ "devDependencies": {
58
+ "@commitlint/cli": "^20.4.1",
59
+ "@commitlint/config-conventional": "^20.4.1",
60
+ "husky": "^9.1.7",
61
+ "knip": "^5.85.0"
62
+ },
63
+ "peerDependencies": {
64
+ "typescript": "^5",
65
+ "@sinclair/typebox": ">= 0.34.0 < 1",
66
+ "@types/bun": "latest",
67
+ "tsup": "^8.5.1",
68
+ "uuid": "^13.0.0"
69
+ },
70
+ "dependencies": {}
58
71
  }