@cosmneo/onion-lasagna-typebox 0.2.0
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/dist/index.cjs +130 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +109 -0
- package/dist/index.d.ts +109 -0
- package/dist/index.js +101 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
Type: () => import_typebox.Type,
|
|
24
|
+
pagination: () => pagination,
|
|
25
|
+
typeboxSchema: () => typeboxSchema
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(src_exports);
|
|
28
|
+
|
|
29
|
+
// src/typebox.adapter.ts
|
|
30
|
+
var import_value = require("@sinclair/typebox/value");
|
|
31
|
+
var import_typebox = require("@sinclair/typebox");
|
|
32
|
+
function safeStringify(value) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.stringify(value);
|
|
35
|
+
} catch {
|
|
36
|
+
if (typeof value === "object" && value !== null) {
|
|
37
|
+
return "[Complex Object]";
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === "bigint") {
|
|
40
|
+
return String(value);
|
|
41
|
+
}
|
|
42
|
+
return String(value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function typeboxSchema(schema) {
|
|
46
|
+
return {
|
|
47
|
+
validate(data) {
|
|
48
|
+
if (!import_value.Value.Check(schema, data)) {
|
|
49
|
+
const errors = [...import_value.Value.Errors(schema, data)];
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
issues: errors.map((error) => ({
|
|
53
|
+
// TypeBox paths are like '/property/nested' - convert to array
|
|
54
|
+
path: error.path.split("/").filter(Boolean),
|
|
55
|
+
message: error.message,
|
|
56
|
+
code: error.type ? String(error.type) : void 0,
|
|
57
|
+
// Use safeStringify to handle circular references and non-serializable values
|
|
58
|
+
expected: error.schema ? safeStringify(error.schema) : void 0,
|
|
59
|
+
received: error.value !== void 0 ? safeStringify(error.value) : void 0
|
|
60
|
+
}))
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const decoded = import_value.Value.Decode(schema, data);
|
|
65
|
+
return { success: true, data: decoded };
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
issues: [
|
|
70
|
+
{
|
|
71
|
+
path: [],
|
|
72
|
+
message: error instanceof Error ? error.message : "Transform decode failed",
|
|
73
|
+
code: "transform_error"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
toJsonSchema(_options) {
|
|
80
|
+
return structuredClone(schema);
|
|
81
|
+
},
|
|
82
|
+
_output: void 0,
|
|
83
|
+
_input: void 0,
|
|
84
|
+
_schema: schema
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/schemas/pagination.ts
|
|
89
|
+
var import_typebox2 = require("@sinclair/typebox");
|
|
90
|
+
var pagination = {
|
|
91
|
+
/**
|
|
92
|
+
* Query params schema for paginated list requests.
|
|
93
|
+
*
|
|
94
|
+
* Uses JSON Schema `default` annotations. Coercion from query string
|
|
95
|
+
* values is handled by the framework (e.g., Fastify).
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* // Direct use
|
|
100
|
+
* typeboxSchema(pagination.input)
|
|
101
|
+
*
|
|
102
|
+
* // Extended with filters
|
|
103
|
+
* typeboxSchema(Type.Intersect([
|
|
104
|
+
* pagination.input,
|
|
105
|
+
* Type.Object({ searchTerm: Type.Optional(Type.String()) }),
|
|
106
|
+
* ]))
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
input: import_typebox2.Type.Object({
|
|
110
|
+
page: import_typebox2.Type.Optional(import_typebox2.Type.Integer({ minimum: 1, default: 1 })),
|
|
111
|
+
pageSize: import_typebox2.Type.Optional(import_typebox2.Type.Integer({ minimum: 1, maximum: 100, default: 10 }))
|
|
112
|
+
}),
|
|
113
|
+
/**
|
|
114
|
+
* Factory for paginated response schemas.
|
|
115
|
+
*
|
|
116
|
+
* @param itemSchema - TypeBox schema for individual items in the list
|
|
117
|
+
* @returns TypeBox schema for `{ items: T[], total: number }`
|
|
118
|
+
*/
|
|
119
|
+
response: (itemSchema) => import_typebox2.Type.Object({
|
|
120
|
+
items: import_typebox2.Type.Array(itemSchema),
|
|
121
|
+
total: import_typebox2.Type.Integer({ minimum: 0 })
|
|
122
|
+
})
|
|
123
|
+
};
|
|
124
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
125
|
+
0 && (module.exports = {
|
|
126
|
+
Type,
|
|
127
|
+
pagination,
|
|
128
|
+
typeboxSchema
|
|
129
|
+
});
|
|
130
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/typebox.adapter.ts","../src/schemas/pagination.ts"],"sourcesContent":["export { typeboxSchema, Type } from './typebox.adapter';\nexport { pagination } from './schemas/pagination';\n","/**\n * @fileoverview TypeBox schema adapter for the unified route system.\n *\n * TypeBox is unique among validation libraries because its schemas ARE\n * JSON Schema. This makes it ideal for OpenAPI generation with zero\n * conversion overhead.\n *\n * @module unified/schema/adapters/typebox\n */\n\nimport type { Static, StaticEncode, TSchema } from '@sinclair/typebox';\nimport { Value } from '@sinclair/typebox/value';\nimport type {\n JsonSchema,\n JsonSchemaOptions,\n SchemaAdapter,\n ValidationResult,\n} from '@cosmneo/onion-lasagna/http/schema/types';\n\n/**\n * Safely stringifies a value for error reporting.\n * Returns undefined if stringification fails (e.g., circular references, BigInt).\n */\nfunction safeStringify(value: unknown): string | undefined {\n try {\n return JSON.stringify(value);\n } catch {\n // Handle circular references, BigInt, or other non-serializable values\n if (typeof value === 'object' && value !== null) {\n return '[Complex Object]';\n }\n if (typeof value === 'bigint') {\n return String(value);\n }\n return String(value);\n }\n}\n\n/**\n * Creates a SchemaAdapter from a TypeBox schema.\n *\n * TypeBox schemas are JSON Schema, so `toJsonSchema()` simply returns\n * the schema itself (with optional cleanup). This makes TypeBox the most\n * efficient choice when OpenAPI generation is a priority.\n *\n * @typeParam T - A TypeBox schema type\n *\n * @param schema - A TypeBox schema to wrap\n * @returns A SchemaAdapter that validates using TypeBox and returns the schema as JSON Schema\n *\n * @example Basic usage\n * ```typescript\n * import { Type } from '@sinclair/typebox';\n * import { typeboxSchema } from '@cosmneo/onion-lasagna-typebox';\n *\n * const userSchema = typeboxSchema(Type.Object({\n * name: Type.String({ minLength: 1, maxLength: 100 }),\n * email: Type.String({ format: 'email' }),\n * age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 })),\n * }));\n *\n * // Validate data\n * const result = userSchema.validate({\n * name: 'John Doe',\n * email: 'john@example.com',\n * });\n *\n * if (result.success) {\n * console.log(result.data); // { name: 'John Doe', email: 'john@example.com' }\n * }\n *\n * // Get JSON Schema (this IS the TypeBox schema!)\n * const jsonSchema = userSchema.toJsonSchema();\n * ```\n *\n * @example Complex types\n * ```typescript\n * const orderSchema = typeboxSchema(Type.Object({\n * id: Type.String({ format: 'uuid' }),\n * items: Type.Array(Type.Object({\n * productId: Type.String(),\n * quantity: Type.Integer({ minimum: 1 }),\n * price: Type.Number({ minimum: 0 }),\n * })),\n * status: Type.Union([\n * Type.Literal('pending'),\n * Type.Literal('processing'),\n * Type.Literal('shipped'),\n * Type.Literal('delivered'),\n * ]),\n * createdAt: Type.String({ format: 'date-time' }),\n * }));\n * ```\n */\nexport function typeboxSchema<T extends TSchema>(\n schema: T,\n): SchemaAdapter<Static<T>, StaticEncode<T>> {\n return {\n validate(data: unknown): ValidationResult<Static<T>> {\n if (!Value.Check(schema, data)) {\n // Collect all validation errors\n const errors = [...Value.Errors(schema, data)];\n\n return {\n success: false,\n issues: errors.map((error) => ({\n // TypeBox paths are like '/property/nested' - convert to array\n path: error.path.split('/').filter(Boolean),\n message: error.message,\n code: error.type ? String(error.type) : undefined,\n // Use safeStringify to handle circular references and non-serializable values\n expected: error.schema ? safeStringify(error.schema) : undefined,\n received: error.value !== undefined ? safeStringify(error.value) : undefined,\n })),\n };\n }\n\n // Apply transforms if any (no-op when schema has no transforms)\n try {\n const decoded = Value.Decode(schema, data);\n return { success: true, data: decoded };\n } catch (error) {\n return {\n success: false,\n issues: [\n {\n path: [],\n message: error instanceof Error ? error.message : 'Transform decode failed',\n code: 'transform_error',\n },\n ],\n };\n }\n },\n\n toJsonSchema(_options?: JsonSchemaOptions): JsonSchema {\n // Deep clone to prevent mutation and strip TypeBox-specific\n // Symbol-keyed metadata ([Kind], etc.), producing clean JSON Schema\n return structuredClone(schema) as JsonSchema;\n },\n\n _output: undefined as Static<T>,\n _input: undefined as StaticEncode<T>,\n _schema: schema,\n };\n}\n\n/**\n * Re-export TypeBox Type for convenience.\n * Users can import TypeBox functionality from this module.\n */\nexport { Type } from '@sinclair/typebox';\n","/**\n * @fileoverview Pre-built TypeBox pagination schemas.\n *\n * Provides reusable schemas for paginated list endpoints,\n * matching the core `PaginationInput` and `PaginatedData<T>` types.\n *\n * TypeBox uses JSON Schema `default` annotations for default values.\n * Frameworks like Fastify handle coercion from strings natively via JSON Schema.\n *\n * @module schemas/pagination\n */\n\nimport { Type, type TSchema } from '@sinclair/typebox';\n\nexport const pagination = {\n /**\n * Query params schema for paginated list requests.\n *\n * Uses JSON Schema `default` annotations. Coercion from query string\n * values is handled by the framework (e.g., Fastify).\n *\n * @example\n * ```typescript\n * // Direct use\n * typeboxSchema(pagination.input)\n *\n * // Extended with filters\n * typeboxSchema(Type.Intersect([\n * pagination.input,\n * Type.Object({ searchTerm: Type.Optional(Type.String()) }),\n * ]))\n * ```\n */\n input: Type.Object({\n page: Type.Optional(Type.Integer({ minimum: 1, default: 1 })),\n pageSize: Type.Optional(Type.Integer({ minimum: 1, maximum: 100, default: 10 })),\n }),\n\n /**\n * Factory for paginated response schemas.\n *\n * @param itemSchema - TypeBox schema for individual items in the list\n * @returns TypeBox schema for `{ items: T[], total: number }`\n */\n response: <T extends TSchema>(itemSchema: T) =>\n Type.Object({\n items: Type.Array(itemSchema),\n total: Type.Integer({ minimum: 0 }),\n }),\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACWA,mBAAsB;AA4ItB,qBAAqB;AAhIrB,SAAS,cAAc,OAAoC;AACzD,MAAI;AACF,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B,QAAQ;AAEN,QAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,aAAO;AAAA,IACT;AACA,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO,OAAO,KAAK;AAAA,IACrB;AACA,WAAO,OAAO,KAAK;AAAA,EACrB;AACF;AA0DO,SAAS,cACd,QAC2C;AAC3C,SAAO;AAAA,IACL,SAAS,MAA4C;AACnD,UAAI,CAAC,mBAAM,MAAM,QAAQ,IAAI,GAAG;AAE9B,cAAM,SAAS,CAAC,GAAG,mBAAM,OAAO,QAAQ,IAAI,CAAC;AAE7C,eAAO;AAAA,UACL,SAAS;AAAA,UACT,QAAQ,OAAO,IAAI,CAAC,WAAW;AAAA;AAAA,YAE7B,MAAM,MAAM,KAAK,MAAM,GAAG,EAAE,OAAO,OAAO;AAAA,YAC1C,SAAS,MAAM;AAAA,YACf,MAAM,MAAM,OAAO,OAAO,MAAM,IAAI,IAAI;AAAA;AAAA,YAExC,UAAU,MAAM,SAAS,cAAc,MAAM,MAAM,IAAI;AAAA,YACvD,UAAU,MAAM,UAAU,SAAY,cAAc,MAAM,KAAK,IAAI;AAAA,UACrE,EAAE;AAAA,QACJ;AAAA,MACF;AAGA,UAAI;AACF,cAAM,UAAU,mBAAM,OAAO,QAAQ,IAAI;AACzC,eAAO,EAAE,SAAS,MAAM,MAAM,QAAQ;AAAA,MACxC,SAAS,OAAO;AACd,eAAO;AAAA,UACL,SAAS;AAAA,UACT,QAAQ;AAAA,YACN;AAAA,cACE,MAAM,CAAC;AAAA,cACP,SAAS,iBAAiB,QAAQ,MAAM,UAAU;AAAA,cAClD,MAAM;AAAA,YACR;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IAEA,aAAa,UAA0C;AAGrD,aAAO,gBAAgB,MAAM;AAAA,IAC/B;AAAA,IAEA,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,SAAS;AAAA,EACX;AACF;;;ACrIA,IAAAA,kBAAmC;AAE5B,IAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBxB,OAAO,qBAAK,OAAO;AAAA,IACjB,MAAM,qBAAK,SAAS,qBAAK,QAAQ,EAAE,SAAS,GAAG,SAAS,EAAE,CAAC,CAAC;AAAA,IAC5D,UAAU,qBAAK,SAAS,qBAAK,QAAQ,EAAE,SAAS,GAAG,SAAS,KAAK,SAAS,GAAG,CAAC,CAAC;AAAA,EACjF,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQD,UAAU,CAAoB,eAC5B,qBAAK,OAAO;AAAA,IACV,OAAO,qBAAK,MAAM,UAAU;AAAA,IAC5B,OAAO,qBAAK,QAAQ,EAAE,SAAS,EAAE,CAAC;AAAA,EACpC,CAAC;AACL;","names":["import_typebox"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as _sinclair_typebox from '@sinclair/typebox';
|
|
2
|
+
import { TSchema, Static, StaticEncode } from '@sinclair/typebox';
|
|
3
|
+
export { Type } from '@sinclair/typebox';
|
|
4
|
+
import { SchemaAdapter } from '@cosmneo/onion-lasagna/http/schema/types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @fileoverview TypeBox schema adapter for the unified route system.
|
|
8
|
+
*
|
|
9
|
+
* TypeBox is unique among validation libraries because its schemas ARE
|
|
10
|
+
* JSON Schema. This makes it ideal for OpenAPI generation with zero
|
|
11
|
+
* conversion overhead.
|
|
12
|
+
*
|
|
13
|
+
* @module unified/schema/adapters/typebox
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a SchemaAdapter from a TypeBox schema.
|
|
18
|
+
*
|
|
19
|
+
* TypeBox schemas are JSON Schema, so `toJsonSchema()` simply returns
|
|
20
|
+
* the schema itself (with optional cleanup). This makes TypeBox the most
|
|
21
|
+
* efficient choice when OpenAPI generation is a priority.
|
|
22
|
+
*
|
|
23
|
+
* @typeParam T - A TypeBox schema type
|
|
24
|
+
*
|
|
25
|
+
* @param schema - A TypeBox schema to wrap
|
|
26
|
+
* @returns A SchemaAdapter that validates using TypeBox and returns the schema as JSON Schema
|
|
27
|
+
*
|
|
28
|
+
* @example Basic usage
|
|
29
|
+
* ```typescript
|
|
30
|
+
* import { Type } from '@sinclair/typebox';
|
|
31
|
+
* import { typeboxSchema } from '@cosmneo/onion-lasagna-typebox';
|
|
32
|
+
*
|
|
33
|
+
* const userSchema = typeboxSchema(Type.Object({
|
|
34
|
+
* name: Type.String({ minLength: 1, maxLength: 100 }),
|
|
35
|
+
* email: Type.String({ format: 'email' }),
|
|
36
|
+
* age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 })),
|
|
37
|
+
* }));
|
|
38
|
+
*
|
|
39
|
+
* // Validate data
|
|
40
|
+
* const result = userSchema.validate({
|
|
41
|
+
* name: 'John Doe',
|
|
42
|
+
* email: 'john@example.com',
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* if (result.success) {
|
|
46
|
+
* console.log(result.data); // { name: 'John Doe', email: 'john@example.com' }
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* // Get JSON Schema (this IS the TypeBox schema!)
|
|
50
|
+
* const jsonSchema = userSchema.toJsonSchema();
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @example Complex types
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const orderSchema = typeboxSchema(Type.Object({
|
|
56
|
+
* id: Type.String({ format: 'uuid' }),
|
|
57
|
+
* items: Type.Array(Type.Object({
|
|
58
|
+
* productId: Type.String(),
|
|
59
|
+
* quantity: Type.Integer({ minimum: 1 }),
|
|
60
|
+
* price: Type.Number({ minimum: 0 }),
|
|
61
|
+
* })),
|
|
62
|
+
* status: Type.Union([
|
|
63
|
+
* Type.Literal('pending'),
|
|
64
|
+
* Type.Literal('processing'),
|
|
65
|
+
* Type.Literal('shipped'),
|
|
66
|
+
* Type.Literal('delivered'),
|
|
67
|
+
* ]),
|
|
68
|
+
* createdAt: Type.String({ format: 'date-time' }),
|
|
69
|
+
* }));
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare function typeboxSchema<T extends TSchema>(schema: T): SchemaAdapter<Static<T>, StaticEncode<T>>;
|
|
73
|
+
|
|
74
|
+
declare const pagination: {
|
|
75
|
+
/**
|
|
76
|
+
* Query params schema for paginated list requests.
|
|
77
|
+
*
|
|
78
|
+
* Uses JSON Schema `default` annotations. Coercion from query string
|
|
79
|
+
* values is handled by the framework (e.g., Fastify).
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* // Direct use
|
|
84
|
+
* typeboxSchema(pagination.input)
|
|
85
|
+
*
|
|
86
|
+
* // Extended with filters
|
|
87
|
+
* typeboxSchema(Type.Intersect([
|
|
88
|
+
* pagination.input,
|
|
89
|
+
* Type.Object({ searchTerm: Type.Optional(Type.String()) }),
|
|
90
|
+
* ]))
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
input: _sinclair_typebox.TObject<{
|
|
94
|
+
page: _sinclair_typebox.TOptional<_sinclair_typebox.TInteger>;
|
|
95
|
+
pageSize: _sinclair_typebox.TOptional<_sinclair_typebox.TInteger>;
|
|
96
|
+
}>;
|
|
97
|
+
/**
|
|
98
|
+
* Factory for paginated response schemas.
|
|
99
|
+
*
|
|
100
|
+
* @param itemSchema - TypeBox schema for individual items in the list
|
|
101
|
+
* @returns TypeBox schema for `{ items: T[], total: number }`
|
|
102
|
+
*/
|
|
103
|
+
response: <T extends TSchema>(itemSchema: T) => _sinclair_typebox.TObject<{
|
|
104
|
+
items: _sinclair_typebox.TArray<T>;
|
|
105
|
+
total: _sinclair_typebox.TInteger;
|
|
106
|
+
}>;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export { pagination, typeboxSchema };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as _sinclair_typebox from '@sinclair/typebox';
|
|
2
|
+
import { TSchema, Static, StaticEncode } from '@sinclair/typebox';
|
|
3
|
+
export { Type } from '@sinclair/typebox';
|
|
4
|
+
import { SchemaAdapter } from '@cosmneo/onion-lasagna/http/schema/types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @fileoverview TypeBox schema adapter for the unified route system.
|
|
8
|
+
*
|
|
9
|
+
* TypeBox is unique among validation libraries because its schemas ARE
|
|
10
|
+
* JSON Schema. This makes it ideal for OpenAPI generation with zero
|
|
11
|
+
* conversion overhead.
|
|
12
|
+
*
|
|
13
|
+
* @module unified/schema/adapters/typebox
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a SchemaAdapter from a TypeBox schema.
|
|
18
|
+
*
|
|
19
|
+
* TypeBox schemas are JSON Schema, so `toJsonSchema()` simply returns
|
|
20
|
+
* the schema itself (with optional cleanup). This makes TypeBox the most
|
|
21
|
+
* efficient choice when OpenAPI generation is a priority.
|
|
22
|
+
*
|
|
23
|
+
* @typeParam T - A TypeBox schema type
|
|
24
|
+
*
|
|
25
|
+
* @param schema - A TypeBox schema to wrap
|
|
26
|
+
* @returns A SchemaAdapter that validates using TypeBox and returns the schema as JSON Schema
|
|
27
|
+
*
|
|
28
|
+
* @example Basic usage
|
|
29
|
+
* ```typescript
|
|
30
|
+
* import { Type } from '@sinclair/typebox';
|
|
31
|
+
* import { typeboxSchema } from '@cosmneo/onion-lasagna-typebox';
|
|
32
|
+
*
|
|
33
|
+
* const userSchema = typeboxSchema(Type.Object({
|
|
34
|
+
* name: Type.String({ minLength: 1, maxLength: 100 }),
|
|
35
|
+
* email: Type.String({ format: 'email' }),
|
|
36
|
+
* age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 })),
|
|
37
|
+
* }));
|
|
38
|
+
*
|
|
39
|
+
* // Validate data
|
|
40
|
+
* const result = userSchema.validate({
|
|
41
|
+
* name: 'John Doe',
|
|
42
|
+
* email: 'john@example.com',
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* if (result.success) {
|
|
46
|
+
* console.log(result.data); // { name: 'John Doe', email: 'john@example.com' }
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* // Get JSON Schema (this IS the TypeBox schema!)
|
|
50
|
+
* const jsonSchema = userSchema.toJsonSchema();
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @example Complex types
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const orderSchema = typeboxSchema(Type.Object({
|
|
56
|
+
* id: Type.String({ format: 'uuid' }),
|
|
57
|
+
* items: Type.Array(Type.Object({
|
|
58
|
+
* productId: Type.String(),
|
|
59
|
+
* quantity: Type.Integer({ minimum: 1 }),
|
|
60
|
+
* price: Type.Number({ minimum: 0 }),
|
|
61
|
+
* })),
|
|
62
|
+
* status: Type.Union([
|
|
63
|
+
* Type.Literal('pending'),
|
|
64
|
+
* Type.Literal('processing'),
|
|
65
|
+
* Type.Literal('shipped'),
|
|
66
|
+
* Type.Literal('delivered'),
|
|
67
|
+
* ]),
|
|
68
|
+
* createdAt: Type.String({ format: 'date-time' }),
|
|
69
|
+
* }));
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare function typeboxSchema<T extends TSchema>(schema: T): SchemaAdapter<Static<T>, StaticEncode<T>>;
|
|
73
|
+
|
|
74
|
+
declare const pagination: {
|
|
75
|
+
/**
|
|
76
|
+
* Query params schema for paginated list requests.
|
|
77
|
+
*
|
|
78
|
+
* Uses JSON Schema `default` annotations. Coercion from query string
|
|
79
|
+
* values is handled by the framework (e.g., Fastify).
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* // Direct use
|
|
84
|
+
* typeboxSchema(pagination.input)
|
|
85
|
+
*
|
|
86
|
+
* // Extended with filters
|
|
87
|
+
* typeboxSchema(Type.Intersect([
|
|
88
|
+
* pagination.input,
|
|
89
|
+
* Type.Object({ searchTerm: Type.Optional(Type.String()) }),
|
|
90
|
+
* ]))
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
input: _sinclair_typebox.TObject<{
|
|
94
|
+
page: _sinclair_typebox.TOptional<_sinclair_typebox.TInteger>;
|
|
95
|
+
pageSize: _sinclair_typebox.TOptional<_sinclair_typebox.TInteger>;
|
|
96
|
+
}>;
|
|
97
|
+
/**
|
|
98
|
+
* Factory for paginated response schemas.
|
|
99
|
+
*
|
|
100
|
+
* @param itemSchema - TypeBox schema for individual items in the list
|
|
101
|
+
* @returns TypeBox schema for `{ items: T[], total: number }`
|
|
102
|
+
*/
|
|
103
|
+
response: <T extends TSchema>(itemSchema: T) => _sinclair_typebox.TObject<{
|
|
104
|
+
items: _sinclair_typebox.TArray<T>;
|
|
105
|
+
total: _sinclair_typebox.TInteger;
|
|
106
|
+
}>;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export { pagination, typeboxSchema };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// src/typebox.adapter.ts
|
|
2
|
+
import { Value } from "@sinclair/typebox/value";
|
|
3
|
+
import { Type } from "@sinclair/typebox";
|
|
4
|
+
function safeStringify(value) {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.stringify(value);
|
|
7
|
+
} catch {
|
|
8
|
+
if (typeof value === "object" && value !== null) {
|
|
9
|
+
return "[Complex Object]";
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === "bigint") {
|
|
12
|
+
return String(value);
|
|
13
|
+
}
|
|
14
|
+
return String(value);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function typeboxSchema(schema) {
|
|
18
|
+
return {
|
|
19
|
+
validate(data) {
|
|
20
|
+
if (!Value.Check(schema, data)) {
|
|
21
|
+
const errors = [...Value.Errors(schema, data)];
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
issues: errors.map((error) => ({
|
|
25
|
+
// TypeBox paths are like '/property/nested' - convert to array
|
|
26
|
+
path: error.path.split("/").filter(Boolean),
|
|
27
|
+
message: error.message,
|
|
28
|
+
code: error.type ? String(error.type) : void 0,
|
|
29
|
+
// Use safeStringify to handle circular references and non-serializable values
|
|
30
|
+
expected: error.schema ? safeStringify(error.schema) : void 0,
|
|
31
|
+
received: error.value !== void 0 ? safeStringify(error.value) : void 0
|
|
32
|
+
}))
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const decoded = Value.Decode(schema, data);
|
|
37
|
+
return { success: true, data: decoded };
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return {
|
|
40
|
+
success: false,
|
|
41
|
+
issues: [
|
|
42
|
+
{
|
|
43
|
+
path: [],
|
|
44
|
+
message: error instanceof Error ? error.message : "Transform decode failed",
|
|
45
|
+
code: "transform_error"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
toJsonSchema(_options) {
|
|
52
|
+
return structuredClone(schema);
|
|
53
|
+
},
|
|
54
|
+
_output: void 0,
|
|
55
|
+
_input: void 0,
|
|
56
|
+
_schema: schema
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/schemas/pagination.ts
|
|
61
|
+
import { Type as Type2 } from "@sinclair/typebox";
|
|
62
|
+
var pagination = {
|
|
63
|
+
/**
|
|
64
|
+
* Query params schema for paginated list requests.
|
|
65
|
+
*
|
|
66
|
+
* Uses JSON Schema `default` annotations. Coercion from query string
|
|
67
|
+
* values is handled by the framework (e.g., Fastify).
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* // Direct use
|
|
72
|
+
* typeboxSchema(pagination.input)
|
|
73
|
+
*
|
|
74
|
+
* // Extended with filters
|
|
75
|
+
* typeboxSchema(Type.Intersect([
|
|
76
|
+
* pagination.input,
|
|
77
|
+
* Type.Object({ searchTerm: Type.Optional(Type.String()) }),
|
|
78
|
+
* ]))
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
input: Type2.Object({
|
|
82
|
+
page: Type2.Optional(Type2.Integer({ minimum: 1, default: 1 })),
|
|
83
|
+
pageSize: Type2.Optional(Type2.Integer({ minimum: 1, maximum: 100, default: 10 }))
|
|
84
|
+
}),
|
|
85
|
+
/**
|
|
86
|
+
* Factory for paginated response schemas.
|
|
87
|
+
*
|
|
88
|
+
* @param itemSchema - TypeBox schema for individual items in the list
|
|
89
|
+
* @returns TypeBox schema for `{ items: T[], total: number }`
|
|
90
|
+
*/
|
|
91
|
+
response: (itemSchema) => Type2.Object({
|
|
92
|
+
items: Type2.Array(itemSchema),
|
|
93
|
+
total: Type2.Integer({ minimum: 0 })
|
|
94
|
+
})
|
|
95
|
+
};
|
|
96
|
+
export {
|
|
97
|
+
Type,
|
|
98
|
+
pagination,
|
|
99
|
+
typeboxSchema
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/typebox.adapter.ts","../src/schemas/pagination.ts"],"sourcesContent":["/**\n * @fileoverview TypeBox schema adapter for the unified route system.\n *\n * TypeBox is unique among validation libraries because its schemas ARE\n * JSON Schema. This makes it ideal for OpenAPI generation with zero\n * conversion overhead.\n *\n * @module unified/schema/adapters/typebox\n */\n\nimport type { Static, StaticEncode, TSchema } from '@sinclair/typebox';\nimport { Value } from '@sinclair/typebox/value';\nimport type {\n JsonSchema,\n JsonSchemaOptions,\n SchemaAdapter,\n ValidationResult,\n} from '@cosmneo/onion-lasagna/http/schema/types';\n\n/**\n * Safely stringifies a value for error reporting.\n * Returns undefined if stringification fails (e.g., circular references, BigInt).\n */\nfunction safeStringify(value: unknown): string | undefined {\n try {\n return JSON.stringify(value);\n } catch {\n // Handle circular references, BigInt, or other non-serializable values\n if (typeof value === 'object' && value !== null) {\n return '[Complex Object]';\n }\n if (typeof value === 'bigint') {\n return String(value);\n }\n return String(value);\n }\n}\n\n/**\n * Creates a SchemaAdapter from a TypeBox schema.\n *\n * TypeBox schemas are JSON Schema, so `toJsonSchema()` simply returns\n * the schema itself (with optional cleanup). This makes TypeBox the most\n * efficient choice when OpenAPI generation is a priority.\n *\n * @typeParam T - A TypeBox schema type\n *\n * @param schema - A TypeBox schema to wrap\n * @returns A SchemaAdapter that validates using TypeBox and returns the schema as JSON Schema\n *\n * @example Basic usage\n * ```typescript\n * import { Type } from '@sinclair/typebox';\n * import { typeboxSchema } from '@cosmneo/onion-lasagna-typebox';\n *\n * const userSchema = typeboxSchema(Type.Object({\n * name: Type.String({ minLength: 1, maxLength: 100 }),\n * email: Type.String({ format: 'email' }),\n * age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 })),\n * }));\n *\n * // Validate data\n * const result = userSchema.validate({\n * name: 'John Doe',\n * email: 'john@example.com',\n * });\n *\n * if (result.success) {\n * console.log(result.data); // { name: 'John Doe', email: 'john@example.com' }\n * }\n *\n * // Get JSON Schema (this IS the TypeBox schema!)\n * const jsonSchema = userSchema.toJsonSchema();\n * ```\n *\n * @example Complex types\n * ```typescript\n * const orderSchema = typeboxSchema(Type.Object({\n * id: Type.String({ format: 'uuid' }),\n * items: Type.Array(Type.Object({\n * productId: Type.String(),\n * quantity: Type.Integer({ minimum: 1 }),\n * price: Type.Number({ minimum: 0 }),\n * })),\n * status: Type.Union([\n * Type.Literal('pending'),\n * Type.Literal('processing'),\n * Type.Literal('shipped'),\n * Type.Literal('delivered'),\n * ]),\n * createdAt: Type.String({ format: 'date-time' }),\n * }));\n * ```\n */\nexport function typeboxSchema<T extends TSchema>(\n schema: T,\n): SchemaAdapter<Static<T>, StaticEncode<T>> {\n return {\n validate(data: unknown): ValidationResult<Static<T>> {\n if (!Value.Check(schema, data)) {\n // Collect all validation errors\n const errors = [...Value.Errors(schema, data)];\n\n return {\n success: false,\n issues: errors.map((error) => ({\n // TypeBox paths are like '/property/nested' - convert to array\n path: error.path.split('/').filter(Boolean),\n message: error.message,\n code: error.type ? String(error.type) : undefined,\n // Use safeStringify to handle circular references and non-serializable values\n expected: error.schema ? safeStringify(error.schema) : undefined,\n received: error.value !== undefined ? safeStringify(error.value) : undefined,\n })),\n };\n }\n\n // Apply transforms if any (no-op when schema has no transforms)\n try {\n const decoded = Value.Decode(schema, data);\n return { success: true, data: decoded };\n } catch (error) {\n return {\n success: false,\n issues: [\n {\n path: [],\n message: error instanceof Error ? error.message : 'Transform decode failed',\n code: 'transform_error',\n },\n ],\n };\n }\n },\n\n toJsonSchema(_options?: JsonSchemaOptions): JsonSchema {\n // Deep clone to prevent mutation and strip TypeBox-specific\n // Symbol-keyed metadata ([Kind], etc.), producing clean JSON Schema\n return structuredClone(schema) as JsonSchema;\n },\n\n _output: undefined as Static<T>,\n _input: undefined as StaticEncode<T>,\n _schema: schema,\n };\n}\n\n/**\n * Re-export TypeBox Type for convenience.\n * Users can import TypeBox functionality from this module.\n */\nexport { Type } from '@sinclair/typebox';\n","/**\n * @fileoverview Pre-built TypeBox pagination schemas.\n *\n * Provides reusable schemas for paginated list endpoints,\n * matching the core `PaginationInput` and `PaginatedData<T>` types.\n *\n * TypeBox uses JSON Schema `default` annotations for default values.\n * Frameworks like Fastify handle coercion from strings natively via JSON Schema.\n *\n * @module schemas/pagination\n */\n\nimport { Type, type TSchema } from '@sinclair/typebox';\n\nexport const pagination = {\n /**\n * Query params schema for paginated list requests.\n *\n * Uses JSON Schema `default` annotations. Coercion from query string\n * values is handled by the framework (e.g., Fastify).\n *\n * @example\n * ```typescript\n * // Direct use\n * typeboxSchema(pagination.input)\n *\n * // Extended with filters\n * typeboxSchema(Type.Intersect([\n * pagination.input,\n * Type.Object({ searchTerm: Type.Optional(Type.String()) }),\n * ]))\n * ```\n */\n input: Type.Object({\n page: Type.Optional(Type.Integer({ minimum: 1, default: 1 })),\n pageSize: Type.Optional(Type.Integer({ minimum: 1, maximum: 100, default: 10 })),\n }),\n\n /**\n * Factory for paginated response schemas.\n *\n * @param itemSchema - TypeBox schema for individual items in the list\n * @returns TypeBox schema for `{ items: T[], total: number }`\n */\n response: <T extends TSchema>(itemSchema: T) =>\n Type.Object({\n items: Type.Array(itemSchema),\n total: Type.Integer({ minimum: 0 }),\n }),\n};\n"],"mappings":";AAWA,SAAS,aAAa;AA4ItB,SAAS,YAAY;AAhIrB,SAAS,cAAc,OAAoC;AACzD,MAAI;AACF,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B,QAAQ;AAEN,QAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,aAAO;AAAA,IACT;AACA,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO,OAAO,KAAK;AAAA,IACrB;AACA,WAAO,OAAO,KAAK;AAAA,EACrB;AACF;AA0DO,SAAS,cACd,QAC2C;AAC3C,SAAO;AAAA,IACL,SAAS,MAA4C;AACnD,UAAI,CAAC,MAAM,MAAM,QAAQ,IAAI,GAAG;AAE9B,cAAM,SAAS,CAAC,GAAG,MAAM,OAAO,QAAQ,IAAI,CAAC;AAE7C,eAAO;AAAA,UACL,SAAS;AAAA,UACT,QAAQ,OAAO,IAAI,CAAC,WAAW;AAAA;AAAA,YAE7B,MAAM,MAAM,KAAK,MAAM,GAAG,EAAE,OAAO,OAAO;AAAA,YAC1C,SAAS,MAAM;AAAA,YACf,MAAM,MAAM,OAAO,OAAO,MAAM,IAAI,IAAI;AAAA;AAAA,YAExC,UAAU,MAAM,SAAS,cAAc,MAAM,MAAM,IAAI;AAAA,YACvD,UAAU,MAAM,UAAU,SAAY,cAAc,MAAM,KAAK,IAAI;AAAA,UACrE,EAAE;AAAA,QACJ;AAAA,MACF;AAGA,UAAI;AACF,cAAM,UAAU,MAAM,OAAO,QAAQ,IAAI;AACzC,eAAO,EAAE,SAAS,MAAM,MAAM,QAAQ;AAAA,MACxC,SAAS,OAAO;AACd,eAAO;AAAA,UACL,SAAS;AAAA,UACT,QAAQ;AAAA,YACN;AAAA,cACE,MAAM,CAAC;AAAA,cACP,SAAS,iBAAiB,QAAQ,MAAM,UAAU;AAAA,cAClD,MAAM;AAAA,YACR;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IAEA,aAAa,UAA0C;AAGrD,aAAO,gBAAgB,MAAM;AAAA,IAC/B;AAAA,IAEA,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,SAAS;AAAA,EACX;AACF;;;ACrIA,SAAS,QAAAA,aAA0B;AAE5B,IAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBxB,OAAOA,MAAK,OAAO;AAAA,IACjB,MAAMA,MAAK,SAASA,MAAK,QAAQ,EAAE,SAAS,GAAG,SAAS,EAAE,CAAC,CAAC;AAAA,IAC5D,UAAUA,MAAK,SAASA,MAAK,QAAQ,EAAE,SAAS,GAAG,SAAS,KAAK,SAAS,GAAG,CAAC,CAAC;AAAA,EACjF,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQD,UAAU,CAAoB,eAC5BA,MAAK,OAAO;AAAA,IACV,OAAOA,MAAK,MAAM,UAAU;AAAA,IAC5B,OAAOA,MAAK,QAAQ,EAAE,SAAS,EAAE,CAAC;AAAA,EACpC,CAAC;AACL;","names":["Type"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cosmneo/onion-lasagna-typebox",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "TypeBox schema adapter for onion-lasagna",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Cosmneo",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Cosmneo/onion-lasagna.git",
|
|
11
|
+
"directory": "packages/schemas/onion-lasagna-typebox"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.0.0"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@sinclair/typebox": "^0.34.41",
|
|
22
|
+
"@cosmneo/onion-lasagna": "workspace:*",
|
|
23
|
+
"tsup": "^8.5.1",
|
|
24
|
+
"vitest": "^4.0.16"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@sinclair/typebox": "^0.34.41",
|
|
28
|
+
"@cosmneo/onion-lasagna": ">=0.2.0"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup",
|
|
32
|
+
"dev": "tsup --watch",
|
|
33
|
+
"test": "vitest",
|
|
34
|
+
"test:run": "vitest run",
|
|
35
|
+
"prepublishOnly": "bun run build"
|
|
36
|
+
},
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"import": "./dist/index.js",
|
|
41
|
+
"require": "./dist/index.cjs",
|
|
42
|
+
"default": "./dist/index.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
}
|
|
48
|
+
}
|