@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 +21 -0
- package/README.md +226 -0
- package/dist/schema/index.cjs +1 -3
- package/dist/schema/index.js +1 -3
- package/package.json +69 -56
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
|
+
[](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
|
package/dist/schema/index.cjs
CHANGED
|
@@ -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) {
|
package/dist/schema/index.js
CHANGED
|
@@ -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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
}
|