@gravito/mass 3.0.0 → 3.0.2

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.js CHANGED
@@ -1,15 +1,536 @@
1
1
  // @bun
2
2
  // src/index.ts
3
- import { tbValidator as tbValidator2 } from "@hono/typebox-validator";
3
+ import { tbValidator as tbValidator3 } from "@hono/typebox-validator";
4
4
  import * as Schema from "@sinclair/typebox";
5
5
 
6
- // src/validator.ts
6
+ // src/coercion.ts
7
7
  import { tbValidator } from "@hono/typebox-validator";
8
+ import { Type } from "@sinclair/typebox";
9
+ function coerceNumber(value) {
10
+ const num = Number(value);
11
+ return Number.isNaN(num) ? NaN : num;
12
+ }
13
+ function coerceInteger(value) {
14
+ const num = Number.parseInt(value, 10);
15
+ return Number.isNaN(num) ? NaN : num;
16
+ }
17
+ function coerceBoolean(value) {
18
+ const lowerValue = value.toLowerCase().trim();
19
+ return ["true", "1", "yes", "on"].includes(lowerValue);
20
+ }
21
+ function coerceDate(value) {
22
+ return new Date(value);
23
+ }
24
+ function CoercibleNumber(options) {
25
+ return Type.Number({
26
+ ...options,
27
+ [Symbol.for("TypeBox.Transform")]: coerceNumber
28
+ });
29
+ }
30
+ function CoercibleInteger(options) {
31
+ return Type.Integer({
32
+ ...options,
33
+ [Symbol.for("TypeBox.Transform")]: coerceInteger
34
+ });
35
+ }
36
+ function CoercibleBoolean(options) {
37
+ return Type.Boolean({
38
+ ...options,
39
+ [Symbol.for("TypeBox.Transform")]: coerceBoolean
40
+ });
41
+ }
42
+ function coerceData(data, schema) {
43
+ if (typeof data !== "object" || data === null) {
44
+ return data;
45
+ }
46
+ const dataObj = data;
47
+ const result = {};
48
+ if (schema.type !== "object" || !schema.properties) {
49
+ return data;
50
+ }
51
+ for (const [key, value] of Object.entries(dataObj)) {
52
+ const propertySchema = schema.properties[key];
53
+ if (!propertySchema) {
54
+ result[key] = value;
55
+ continue;
56
+ }
57
+ if (typeof value === "string") {
58
+ if (propertySchema.type === "number" || propertySchema.type === "integer") {
59
+ result[key] = propertySchema.type === "integer" ? coerceInteger(value) : coerceNumber(value);
60
+ } else if (propertySchema.type === "boolean") {
61
+ result[key] = coerceBoolean(value);
62
+ } else {
63
+ result[key] = value;
64
+ }
65
+ } else {
66
+ result[key] = value;
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+ function validateWithCoercion(source, schema, hook) {
72
+ return async (c, next) => {
73
+ let data;
74
+ switch (source) {
75
+ case "json":
76
+ data = await c.req.json();
77
+ break;
78
+ case "query":
79
+ data = c.req.query();
80
+ break;
81
+ case "param":
82
+ data = c.req.param();
83
+ break;
84
+ case "form":
85
+ data = await c.req.parseBody();
86
+ break;
87
+ default:
88
+ data = {};
89
+ }
90
+ if (source === "query" || source === "param") {
91
+ data = coerceData(data, schema);
92
+ }
93
+ const validator = tbValidator(source, schema, hook);
94
+ const originalQueryMethod = c.req.query;
95
+ const originalParamMethod = c.req.param;
96
+ if (source === "query") {
97
+ c.req.query = () => data;
98
+ } else if (source === "param") {
99
+ c.req.param = () => data;
100
+ }
101
+ await validator(c, next);
102
+ if (source === "query") {
103
+ c.req.query = originalQueryMethod;
104
+ } else if (source === "param") {
105
+ c.req.param = originalParamMethod;
106
+ }
107
+ };
108
+ }
109
+ // src/errors.ts
110
+ var ERROR_MESSAGES_ZH_TW = {
111
+ REQUIRED: "\u6B64\u6B04\u4F4D\u70BA\u5FC5\u586B",
112
+ INVALID_TYPE: "\u8CC7\u6599\u578B\u5225\u4E0D\u6B63\u78BA",
113
+ INVALID_EMAIL: "\u96FB\u5B50\u90F5\u4EF6\u683C\u5F0F\u4E0D\u6B63\u78BA",
114
+ INVALID_URL: "\u7DB2\u5740\u683C\u5F0F\u4E0D\u6B63\u78BA",
115
+ INVALID_UUID: "UUID \u683C\u5F0F\u4E0D\u6B63\u78BA",
116
+ INVALID_DATE: "\u65E5\u671F\u683C\u5F0F\u4E0D\u6B63\u78BA",
117
+ TOO_SHORT: "\u9577\u5EA6\u904E\u77ED",
118
+ TOO_LONG: "\u9577\u5EA6\u904E\u9577",
119
+ TOO_SMALL: "\u6578\u503C\u904E\u5C0F",
120
+ TOO_LARGE: "\u6578\u503C\u904E\u5927",
121
+ INVALID_PATTERN: "\u683C\u5F0F\u4E0D\u7B26\u5408\u898F\u5247",
122
+ INVALID_FORMAT: "\u683C\u5F0F\u4E0D\u6B63\u78BA"
123
+ };
124
+ var ERROR_MESSAGES_EN = {
125
+ REQUIRED: "This field is required",
126
+ INVALID_TYPE: "Invalid data type",
127
+ INVALID_EMAIL: "Invalid email format",
128
+ INVALID_URL: "Invalid URL format",
129
+ INVALID_UUID: "Invalid UUID format",
130
+ INVALID_DATE: "Invalid date format",
131
+ TOO_SHORT: "Too short",
132
+ TOO_LONG: "Too long",
133
+ TOO_SMALL: "Value too small",
134
+ TOO_LARGE: "Value too large",
135
+ INVALID_PATTERN: "Does not match required pattern",
136
+ INVALID_FORMAT: "Invalid format"
137
+ };
138
+
139
+ class MassValidationError extends Error {
140
+ source;
141
+ errors;
142
+ constructor(source, errors) {
143
+ super(`Validation failed for ${source}`);
144
+ this.source = source;
145
+ this.errors = errors;
146
+ this.name = "MassValidationError";
147
+ }
148
+ toJSON() {
149
+ return {
150
+ error: "ValidationError",
151
+ source: this.source,
152
+ details: this.errors
153
+ };
154
+ }
155
+ }
156
+ function enhanceError(error, locale = "zh-TW") {
157
+ const messages = locale === "zh-TW" ? ERROR_MESSAGES_ZH_TW : ERROR_MESSAGES_EN;
158
+ const { path, message, schema, value } = error;
159
+ let enhancedMessage = message;
160
+ if (message.includes("required property")) {
161
+ enhancedMessage = messages.REQUIRED;
162
+ } else if (message.includes("email")) {
163
+ enhancedMessage = messages.INVALID_EMAIL;
164
+ } else if (message.includes("url")) {
165
+ enhancedMessage = messages.INVALID_URL;
166
+ } else if (message.includes("uuid")) {
167
+ enhancedMessage = messages.INVALID_UUID;
168
+ } else if (message.includes("date-time") || message.includes("date")) {
169
+ enhancedMessage = messages.INVALID_DATE;
170
+ } else if (message.includes("length greater or equal")) {
171
+ const minLength = schema.minLength;
172
+ enhancedMessage = locale === "zh-TW" ? `${messages.TOO_SHORT}\uFF08\u6700\u5C11 ${minLength} \u500B\u5B57\u5143\uFF09` : `${messages.TOO_SHORT} (minimum ${minLength} characters)`;
173
+ } else if (message.includes("length less or equal")) {
174
+ const maxLength = schema.maxLength;
175
+ enhancedMessage = locale === "zh-TW" ? `${messages.TOO_LONG}\uFF08\u6700\u591A ${maxLength} \u500B\u5B57\u5143\uFF09` : `${messages.TOO_LONG} (maximum ${maxLength} characters)`;
176
+ } else if (message.includes("greater or equal")) {
177
+ const minimum = schema.minimum;
178
+ enhancedMessage = locale === "zh-TW" ? `${messages.TOO_SMALL}\uFF08\u6700\u5C0F\u503C\uFF1A${minimum}\uFF09` : `${messages.TOO_SMALL} (minimum: ${minimum})`;
179
+ } else if (message.includes("less or equal")) {
180
+ const maximum = schema.maximum;
181
+ enhancedMessage = locale === "zh-TW" ? `${messages.TOO_LARGE}\uFF08\u6700\u5927\u503C\uFF1A${maximum}\uFF09` : `${messages.TOO_LARGE} (maximum: ${maximum})`;
182
+ } else if (message.includes("to match")) {
183
+ enhancedMessage = messages.INVALID_PATTERN;
184
+ } else if (message.includes("format")) {
185
+ enhancedMessage = messages.INVALID_FORMAT;
186
+ }
187
+ return {
188
+ path,
189
+ message: enhancedMessage,
190
+ expected: schema.type ? String(schema.type) : undefined,
191
+ received: typeof value
192
+ };
193
+ }
194
+ function createErrorFormatter(formatter) {
195
+ return (error) => {
196
+ const formatted = formatter(error);
197
+ return {
198
+ path: formatted.path,
199
+ message: formatted.message,
200
+ expected: formatted.expected,
201
+ received: formatted.received
202
+ };
203
+ };
204
+ }
205
+ function formatErrors(errors) {
206
+ const fields = {};
207
+ for (const err of errors) {
208
+ const fieldName = err.path.replace(/^\//, "").replace(/\//g, ".");
209
+ if (!fields[fieldName]) {
210
+ fields[fieldName] = [];
211
+ }
212
+ fields[fieldName].push(err.message);
213
+ }
214
+ return { fields };
215
+ }
216
+ // src/formats.ts
217
+ import { FormatRegistry } from "@sinclair/typebox";
218
+ var REGISTERED_FORMATS = [
219
+ "email",
220
+ "uri",
221
+ "uri-reference",
222
+ "uuid",
223
+ "date-time",
224
+ "date",
225
+ "time",
226
+ "ipv4",
227
+ "ipv6"
228
+ ];
229
+ var EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
230
+ var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
231
+ var URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/;
232
+ var URI_REFERENCE_REGEX = /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/\/)?[^\s]*$/i;
233
+ var DATE_TIME_REGEX = /^\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:\d{2})?$/;
234
+ var DATE_REGEX = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
235
+ var TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)(?:\.\d{3})?$/;
236
+ var IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
237
+ var IPV6_REGEX = /^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:))$/;
238
+ var FORMAT_VALIDATORS = {
239
+ email: (value) => EMAIL_REGEX.test(value),
240
+ uri: (value) => URL_REGEX.test(value),
241
+ "uri-reference": (value) => URI_REFERENCE_REGEX.test(value),
242
+ uuid: (value) => UUID_REGEX.test(value),
243
+ "date-time": (value) => DATE_TIME_REGEX.test(value),
244
+ date: (value) => DATE_REGEX.test(value),
245
+ time: (value) => TIME_REGEX.test(value),
246
+ ipv4: (value) => IPV4_REGEX.test(value),
247
+ ipv6: (value) => IPV6_REGEX.test(value)
248
+ };
249
+ function registerFormat(name, validator) {
250
+ FormatRegistry.Set(name, validator);
251
+ }
252
+ function registerAllFormats() {
253
+ for (const [name, validator] of Object.entries(FORMAT_VALIDATORS)) {
254
+ FormatRegistry.Set(name, validator);
255
+ }
256
+ }
257
+ function isFormatRegistered(name) {
258
+ return FormatRegistry.Has(name);
259
+ }
260
+ function getFormatValidator(name) {
261
+ return FormatRegistry.Has(name) ? FormatRegistry.Get(name) : undefined;
262
+ }
263
+ function unregisterFormat(name) {
264
+ FormatRegistry.Delete(name);
265
+ }
266
+ // src/openapi.ts
267
+ import { Kind } from "@sinclair/typebox";
268
+ function typeboxToOpenApi(schema) {
269
+ const result = {};
270
+ if (schema.description) {
271
+ result.description = schema.description;
272
+ }
273
+ if (schema.default !== undefined) {
274
+ result.default = schema.default;
275
+ }
276
+ if (schema.examples && Array.isArray(schema.examples) && schema.examples.length > 0) {
277
+ result.example = schema.examples[0];
278
+ }
279
+ switch (schema[Kind]) {
280
+ case "String": {
281
+ const stringSchema = schema;
282
+ result.type = "string";
283
+ if (stringSchema.format) {
284
+ result.format = stringSchema.format;
285
+ }
286
+ if (stringSchema.minLength !== undefined) {
287
+ result.minLength = stringSchema.minLength;
288
+ }
289
+ if (stringSchema.maxLength !== undefined) {
290
+ result.maxLength = stringSchema.maxLength;
291
+ }
292
+ if (stringSchema.pattern) {
293
+ result.pattern = stringSchema.pattern;
294
+ }
295
+ break;
296
+ }
297
+ case "Number":
298
+ case "Integer": {
299
+ const numberSchema = schema;
300
+ result.type = schema[Kind] === "Integer" ? "integer" : "number";
301
+ if (numberSchema.minimum !== undefined) {
302
+ result.minimum = numberSchema.minimum;
303
+ }
304
+ if (numberSchema.maximum !== undefined) {
305
+ result.maximum = numberSchema.maximum;
306
+ }
307
+ break;
308
+ }
309
+ case "Boolean": {
310
+ result.type = "boolean";
311
+ break;
312
+ }
313
+ case "Array": {
314
+ const arraySchema = schema;
315
+ result.type = "array";
316
+ if (arraySchema.items) {
317
+ result.items = typeboxToOpenApi(arraySchema.items);
318
+ }
319
+ if (arraySchema.minItems !== undefined) {
320
+ result.minItems = arraySchema.minItems;
321
+ }
322
+ if (arraySchema.maxItems !== undefined) {
323
+ result.maxItems = arraySchema.maxItems;
324
+ }
325
+ break;
326
+ }
327
+ case "Object": {
328
+ const objectSchema = schema;
329
+ result.type = "object";
330
+ if (objectSchema.properties) {
331
+ result.properties = {};
332
+ const required = [];
333
+ for (const [key, value] of Object.entries(objectSchema.properties)) {
334
+ const optionalSymbol = Symbol.for("TypeBox.Optional");
335
+ const isOptional = value[optionalSymbol] === "Optional";
336
+ if (isOptional) {
337
+ result.properties[key] = typeboxToOpenApi(value);
338
+ } else {
339
+ result.properties[key] = typeboxToOpenApi(value);
340
+ required.push(key);
341
+ }
342
+ }
343
+ if (required.length > 0) {
344
+ result.required = required;
345
+ }
346
+ }
347
+ if (objectSchema.additionalProperties !== undefined) {
348
+ if (typeof objectSchema.additionalProperties === "boolean") {
349
+ result.additionalProperties = objectSchema.additionalProperties;
350
+ } else {
351
+ result.additionalProperties = typeboxToOpenApi(objectSchema.additionalProperties);
352
+ }
353
+ }
354
+ break;
355
+ }
356
+ case "Union": {
357
+ if (schema.anyOf && Array.isArray(schema.anyOf)) {
358
+ result.anyOf = schema.anyOf.map((s) => typeboxToOpenApi(s));
359
+ }
360
+ break;
361
+ }
362
+ case "Intersect": {
363
+ if (schema.allOf && Array.isArray(schema.allOf)) {
364
+ result.allOf = schema.allOf.map((s) => typeboxToOpenApi(s));
365
+ }
366
+ break;
367
+ }
368
+ case "Literal": {
369
+ if (schema.const !== undefined) {
370
+ result.enum = [schema.const];
371
+ result.type = typeof schema.const;
372
+ }
373
+ break;
374
+ }
375
+ case "Enum": {
376
+ if (schema.anyOf && Array.isArray(schema.anyOf)) {
377
+ result.enum = schema.anyOf.map((s) => s.const);
378
+ if (result.enum.length > 0) {
379
+ result.type = typeof result.enum[0];
380
+ }
381
+ }
382
+ break;
383
+ }
384
+ case "Null": {
385
+ result.type = "null";
386
+ break;
387
+ }
388
+ case "Optional": {
389
+ if (schema[Kind] === "Optional" && schema.anyOf && Array.isArray(schema.anyOf)) {
390
+ const innerSchema = schema.anyOf[0];
391
+ return typeboxToOpenApi(innerSchema);
392
+ }
393
+ break;
394
+ }
395
+ default: {
396
+ if (schema.type) {
397
+ result.type = schema.type;
398
+ }
399
+ }
400
+ }
401
+ return result;
402
+ }
403
+ function createAstralResource(options) {
404
+ const {
405
+ path,
406
+ method,
407
+ summary,
408
+ description,
409
+ tags,
410
+ requestSchema,
411
+ requestDescription,
412
+ requestRequired = true,
413
+ responseSchema,
414
+ responseDescription = "Successful response",
415
+ responseStatusCode = 200
416
+ } = options;
417
+ const resource = {
418
+ path,
419
+ method: method.toUpperCase(),
420
+ responses: {
421
+ [responseStatusCode]: {
422
+ description: responseDescription,
423
+ content: {
424
+ "application/json": {
425
+ schema: typeboxToOpenApi(responseSchema)
426
+ }
427
+ }
428
+ }
429
+ }
430
+ };
431
+ if (summary) {
432
+ resource.summary = summary;
433
+ }
434
+ if (description) {
435
+ resource.description = description;
436
+ }
437
+ if (tags && tags.length > 0) {
438
+ resource.tags = tags;
439
+ }
440
+ if (requestSchema && ["POST", "PUT", "PATCH"].includes(method.toUpperCase())) {
441
+ resource.requestBody = {
442
+ description: requestDescription,
443
+ required: requestRequired,
444
+ content: {
445
+ "application/json": {
446
+ schema: typeboxToOpenApi(requestSchema)
447
+ }
448
+ }
449
+ };
450
+ }
451
+ return resource;
452
+ }
453
+ function createCrudResources(options) {
454
+ const { resourceName, basePath, tags = [resourceName], schemas } = options;
455
+ const capitalizedName = resourceName.charAt(0).toUpperCase() + resourceName.slice(1);
456
+ return {
457
+ list: createAstralResource({
458
+ path: basePath,
459
+ method: "GET",
460
+ summary: `Get ${capitalizedName} List`,
461
+ tags,
462
+ responseSchema: schemas.list || schemas.item
463
+ }),
464
+ get: createAstralResource({
465
+ path: `${basePath}/:id`,
466
+ method: "GET",
467
+ summary: `Get Single ${capitalizedName}`,
468
+ tags,
469
+ responseSchema: schemas.item
470
+ }),
471
+ create: createAstralResource({
472
+ path: basePath,
473
+ method: "POST",
474
+ summary: `Create ${capitalizedName}`,
475
+ tags,
476
+ requestSchema: schemas.create,
477
+ responseSchema: schemas.item,
478
+ responseStatusCode: 201
479
+ }),
480
+ update: createAstralResource({
481
+ path: `${basePath}/:id`,
482
+ method: "PATCH",
483
+ summary: `Update ${capitalizedName}`,
484
+ tags,
485
+ requestSchema: schemas.update,
486
+ responseSchema: schemas.item
487
+ }),
488
+ delete: createAstralResource({
489
+ path: `${basePath}/:id`,
490
+ method: "DELETE",
491
+ summary: `Delete ${capitalizedName}`,
492
+ tags,
493
+ responseSchema: schemas.item
494
+ })
495
+ };
496
+ }
497
+ // src/utils.ts
498
+ import { Type as Type2 } from "@sinclair/typebox";
499
+ function partial(schema) {
500
+ return Type2.Partial(schema);
501
+ }
502
+ // src/validator.ts
503
+ import { tbValidator as tbValidator2 } from "@hono/typebox-validator";
8
504
  function validate(source, schema, hook) {
9
- return tbValidator(source, schema, hook);
505
+ return tbValidator2(source, schema, hook);
10
506
  }
11
507
  export {
12
- tbValidator2 as validator,
508
+ tbValidator3 as validator,
509
+ validateWithCoercion,
13
510
  validate,
14
- Schema
511
+ unregisterFormat,
512
+ typeboxToOpenApi,
513
+ registerFormat,
514
+ registerAllFormats,
515
+ partial,
516
+ isFormatRegistered,
517
+ getFormatValidator,
518
+ formatErrors,
519
+ enhanceError,
520
+ createErrorFormatter,
521
+ createCrudResources,
522
+ createAstralResource,
523
+ coerceNumber,
524
+ coerceInteger,
525
+ coerceDate,
526
+ coerceData,
527
+ coerceBoolean,
528
+ Schema,
529
+ REGISTERED_FORMATS,
530
+ MassValidationError,
531
+ ERROR_MESSAGES_ZH_TW,
532
+ ERROR_MESSAGES_EN,
533
+ CoercibleNumber,
534
+ CoercibleInteger,
535
+ CoercibleBoolean
15
536
  };