@kalutskii/foundation 0.6.21 → 0.6.23
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/README.md +21 -14
- package/dist/index.d.ts +98 -33
- package/dist/index.js +28 -6
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -144,7 +144,7 @@ schema.parse({ page: '2', isActive: 'true' }); // { page: 2, isActive: true }
|
|
|
144
144
|
|
|
145
145
|
`asQuery` preprocesses string values from query parameters — coercing `'true'`/`'false'` to booleans and numeric strings to numbers — before passing them to the Zod schema for validation.
|
|
146
146
|
|
|
147
|
-
###
|
|
147
|
+
### 7. Build flat query schemas from nested objects
|
|
148
148
|
|
|
149
149
|
```typescript
|
|
150
150
|
import { asQuerySchema } from '@kalutskii/foundation';
|
|
@@ -171,26 +171,32 @@ schema.parse({ sort: { field: 'name', order: 'asc' } }); // throws
|
|
|
171
171
|
|
|
172
172
|
`asQuerySchema` flattens nested `z.object(...)` fields to the top level, making it suitable for flat query parameter validation. Nested object fields are lifted, while optionality is preserved: required parents keep their fields required, optional parents make all promoted fields optional. The result is a strict schema (unknown keys are rejected).
|
|
173
173
|
|
|
174
|
-
### 8.
|
|
174
|
+
### 8. Build reusable search schemas
|
|
175
175
|
|
|
176
176
|
```typescript
|
|
177
|
-
import {
|
|
177
|
+
import { zodPaginationSchema, zodSearchSchema } from '@kalutskii/foundation';
|
|
178
178
|
import { z } from 'zod';
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
const assetSchema = z.object({
|
|
181
|
+
status: z.enum(['active', 'archived']),
|
|
182
|
+
categoryId: z.number(),
|
|
183
|
+
});
|
|
183
184
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
search: z.string().optional(),
|
|
187
|
-
...zodPaginationShape,
|
|
185
|
+
const assetSearchSchema = zodSearchSchema({
|
|
186
|
+
filters: assetSchema.pick({ status: true, categoryId: true }),
|
|
188
187
|
});
|
|
189
188
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
type AssetSearch = z.infer<typeof assetSearchSchema>;
|
|
190
|
+
|
|
191
|
+
assetSearchSchema.parse({
|
|
192
|
+
where: { status: 'active' },
|
|
193
|
+
query: 'asset name',
|
|
194
|
+
pagination: { offset: 0, limit: 20 },
|
|
193
195
|
});
|
|
196
|
+
|
|
197
|
+
// Pagination remains available as a standalone reusable schema.
|
|
198
|
+
zodPaginationSchema.parse({ offset: '0', limit: '20' });
|
|
199
|
+
// { offset: 0, limit: 20 }
|
|
194
200
|
```
|
|
195
201
|
|
|
196
202
|
### 9. Work with JWT using `ZodJWTService`
|
|
@@ -240,7 +246,8 @@ The package exports all public APIs from a single entrypoint:
|
|
|
240
246
|
- **Hono**: `respond`, `onHandlerError`, `honoLoggingHandler`.
|
|
241
247
|
- **Utilities**: `safeExecute`, `measureExecutionTime`, `generateRandomString`, `log`, `getColoredHTTPStatus`, datetime helpers.
|
|
242
248
|
- **Zod JWT**: `ZodJWTService`.
|
|
243
|
-
- **Zod
|
|
249
|
+
- **Zod Search**: `zodSearchSchema`, `ZodSearchSchemaOptions`.
|
|
250
|
+
- **Zod Pagination**: `zodPaginationSchema`, `zodPaginationShape`, `ZodPaginationOptions`.
|
|
244
251
|
- **Zod Flatten**: `asQuerySchema`, `flattenZodShape`, `isZodObject`, `isZodOptional`, `unwrapOptional`, `FlattenZodShape`.
|
|
245
252
|
- **Zod Validation**: `asQuery`, `AsQuery`, `AtLeastOne`.
|
|
246
253
|
- **Type Utilities**: `Simplify`.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SQL
|
|
1
|
+
import { SQL } from 'drizzle-orm';
|
|
2
2
|
import { ErrorHandler, MiddlewareHandler, Context, TypedResponse } from 'hono';
|
|
3
3
|
import { Locale } from 'date-fns';
|
|
4
4
|
import { SymmetricAlgorithm } from 'hono/utils/jwt/jwa';
|
|
@@ -58,29 +58,6 @@ type FetchResult<T> = {
|
|
|
58
58
|
data: null;
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
type QueryPrimitive = string | number | boolean | bigint | Date;
|
|
62
|
-
/**
|
|
63
|
-
* Recursively transforms all fields of T to `string`, matching how query parameters are serialized.
|
|
64
|
-
* Handles nested objects, arrays, and primitive values (including null and undefined as optional).
|
|
65
|
-
*/
|
|
66
|
-
type AsQuery<T> = T extends null | undefined ? T : T extends QueryPrimitive ? string : T extends readonly (infer Item)[] ? AsQuery<Item>[] : T extends object ? {
|
|
67
|
-
[Key in keyof T]: AsQuery<T[Key]>;
|
|
68
|
-
} : string;
|
|
69
|
-
/**
|
|
70
|
-
* Utility type that ensures at least one property from the specified
|
|
71
|
-
* keys of a given type T is required, while the rest remain optional.
|
|
72
|
-
*
|
|
73
|
-
* This is useful for scenarios where you want to enforce that at least one
|
|
74
|
-
* of several optional (nullable) properties must be provided in an object.
|
|
75
|
-
*
|
|
76
|
-
* ```typescript
|
|
77
|
-
* type Example = AtLeastOne<{ a?: string; b?: number; c?: boolean }>;
|
|
78
|
-
* // Valid: { a: "hello" }, { b: 42 }, { c: true }, { a: "hello", b: 42 }
|
|
79
|
-
* // Invalid: {}, { a: undefined, b: undefined, c: undefined }
|
|
80
|
-
* ```
|
|
81
|
-
*/
|
|
82
|
-
type AtLeastOne<T, Keys extends keyof T = keyof T> = Keys extends keyof T ? Simplify$1<Required<Pick<T, Keys>> & Partial<Omit<T, Keys>>> : never;
|
|
83
|
-
|
|
84
61
|
/**
|
|
85
62
|
* Wraps c.json with a typed success payload / (or void data) & possible APIError response.
|
|
86
63
|
* When no data is provided, responds with an empty object {} (purely for type consistency).
|
|
@@ -88,7 +65,7 @@ type AtLeastOne<T, Keys extends keyof T = keyof T> = Keys extends keyof T ? Simp
|
|
|
88
65
|
declare function respond<T extends object = Record<string, never>, S extends SuccessStatusCode = SuccessStatusCode>(c: Context, options: {
|
|
89
66
|
status: S;
|
|
90
67
|
data?: T;
|
|
91
|
-
}): Response & TypedResponse<APISuccess<
|
|
68
|
+
}): Response & TypedResponse<APISuccess<T> | APIError, S, 'json'>;
|
|
92
69
|
|
|
93
70
|
/**
|
|
94
71
|
* Factory for creating successful API responses with serializable data.
|
|
@@ -297,26 +274,114 @@ declare class ZodJWTService<TPayloadOrSchema> {
|
|
|
297
274
|
verifyOrThrow(token: string, secret: string): Promise<Payload<TPayloadOrSchema>>;
|
|
298
275
|
}
|
|
299
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Validates offset-based pagination shared by search request contracts.
|
|
279
|
+
* Missing fields inside the required object receive predictable defaults.
|
|
280
|
+
*/
|
|
300
281
|
declare const zodPaginationSchema: z.ZodObject<{
|
|
301
282
|
offset: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
302
283
|
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
303
284
|
}, z.core.$strip>;
|
|
304
285
|
/**
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
* Spread into another `z.object(...)` when composing a larger query schema,
|
|
309
|
-
* or use `zodPaginationSchema.extend(...)` when building directly from the schema.
|
|
286
|
+
* Exposes pagination fields for composition into other object schemas.
|
|
287
|
+
* The shape stays derived from the schema to prevent contract divergence.
|
|
310
288
|
*/
|
|
311
289
|
declare const zodPaginationShape: {
|
|
312
290
|
offset: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
313
291
|
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
314
292
|
};
|
|
293
|
+
|
|
315
294
|
/**
|
|
316
|
-
* TypeScript
|
|
317
|
-
*
|
|
295
|
+
* TypeScript output produced after successful pagination validation.
|
|
296
|
+
* Defaulted fields are represented as required numeric properties.
|
|
318
297
|
*/
|
|
319
298
|
type ZodPaginationOptions = z.infer<typeof zodPaginationSchema>;
|
|
299
|
+
/**
|
|
300
|
+
* Configuration used to compose a reusable search request schema.
|
|
301
|
+
* Literal feature flags are preserved in the resulting static shape.
|
|
302
|
+
*/
|
|
303
|
+
type ZodSearchSchemaOptions<TShape extends z.ZodRawShape, TQueryEnabled extends boolean = true, TPaginationEnabled extends boolean = true> = {
|
|
304
|
+
/**
|
|
305
|
+
* Object schema whose fields become available inside the `where` object.
|
|
306
|
+
* Every resulting field is optional, but one value must be defined.
|
|
307
|
+
*/
|
|
308
|
+
filters: z.ZodObject<TShape>;
|
|
309
|
+
/**
|
|
310
|
+
* Controls whether an optional non-empty `query` field is generated.
|
|
311
|
+
* The field is enabled when this option is omitted from the call.
|
|
312
|
+
*/
|
|
313
|
+
queryEnabled?: TQueryEnabled;
|
|
314
|
+
/**
|
|
315
|
+
* Controls whether a required `pagination` object is generated.
|
|
316
|
+
* The field is enabled when this option is omitted from the call.
|
|
317
|
+
*/
|
|
318
|
+
paginationEnabled?: TPaginationEnabled;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Optional text query shared by search request contracts.
|
|
323
|
+
* Provided values are trimmed and must contain visible text.
|
|
324
|
+
*/
|
|
325
|
+
declare const zodSearchQuerySchema: z.ZodOptional<z.ZodString>;
|
|
326
|
+
/**
|
|
327
|
+
* Makes every supplied filter optional while requiring one defined value.
|
|
328
|
+
* The wrapper itself stays optional because searches may omit filtering.
|
|
329
|
+
*/
|
|
330
|
+
declare const createZodSearchWhereSchema: <TShape extends z.ZodRawShape>(filters: z.ZodObject<TShape>) => z.ZodOptional<z.ZodPipe<z.ZodObject<{ -readonly [k in keyof TShape]: z.ZodOptional<TShape[k]>; }, z.core.$strip>, z.ZodTransform<Awaited<AtLeastOne<z.core.$InferObjectOutput<{ -readonly [k in keyof TShape]: z.ZodOptional<TShape[k]>; }, {}>, keyof z.core.$InferObjectOutput<{ -readonly [k in keyof TShape]: z.ZodOptional<TShape[k]>; }, {}>>>, z.core.$InferObjectOutput<{ -readonly [k in keyof TShape]: z.ZodOptional<TShape[k]>; }, {}>>>>;
|
|
331
|
+
/**
|
|
332
|
+
* Empty schema shape used when a search feature is explicitly disabled.
|
|
333
|
+
* Intersections remove disabled fields without widening enabled branches.
|
|
334
|
+
*/
|
|
335
|
+
type ZodDisabledSearchShape = Record<never, never>;
|
|
336
|
+
/**
|
|
337
|
+
* Schema-level shape assembled from filter fields and literal feature flags.
|
|
338
|
+
* Keeping Zod schemas here makes runtime composition the contract source.
|
|
339
|
+
*/
|
|
340
|
+
type ZodSearchShape<TShape extends z.ZodRawShape, TQueryEnabled extends boolean, TPaginationEnabled extends boolean> = {
|
|
341
|
+
where: ReturnType<typeof createZodSearchWhereSchema<TShape>>;
|
|
342
|
+
} & (TQueryEnabled extends false ? ZodDisabledSearchShape : {
|
|
343
|
+
query: typeof zodSearchQuerySchema;
|
|
344
|
+
}) & (TPaginationEnabled extends false ? ZodDisabledSearchShape : {
|
|
345
|
+
pagination: typeof zodPaginationSchema;
|
|
346
|
+
});
|
|
347
|
+
/**
|
|
348
|
+
* Builds a strict schema for reusable search request contracts.
|
|
349
|
+
* The optional `where` object must contain one defined filter.
|
|
350
|
+
*
|
|
351
|
+
* Query is optional and non-empty; pagination is required by default.
|
|
352
|
+
* Literal feature flags update both runtime and inferred static shapes.
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* const assetSearchSchema = zodSearchSchema({
|
|
356
|
+
* filters: assetSchema.pick({ status: true }),
|
|
357
|
+
* });
|
|
358
|
+
*
|
|
359
|
+
* type AssetSearch = z.infer<typeof assetSearchSchema>;
|
|
360
|
+
*/
|
|
361
|
+
declare const zodSearchSchema: <const TShape extends z.ZodRawShape, const TQueryEnabled extends boolean = true, const TPaginationEnabled extends boolean = true>({ filters, queryEnabled, paginationEnabled, }: ZodSearchSchemaOptions<TShape, TQueryEnabled, TPaginationEnabled>) => z.ZodObject<ZodSearchShape<TShape, TQueryEnabled, TPaginationEnabled> extends infer T ? { -readonly [P in keyof T]: T[P]; } : never, z.core.$strict>;
|
|
362
|
+
|
|
363
|
+
type QueryPrimitive = string | number | boolean | bigint | Date;
|
|
364
|
+
/**
|
|
365
|
+
* Recursively transforms all fields of T to `string`, matching how query parameters are serialized.
|
|
366
|
+
* Handles nested objects, arrays, and primitive values (including null and undefined as optional).
|
|
367
|
+
*/
|
|
368
|
+
type AsQuery<T> = T extends null | undefined ? T : T extends QueryPrimitive ? string : T extends readonly (infer Item)[] ? AsQuery<Item>[] : T extends object ? {
|
|
369
|
+
[Key in keyof T]: AsQuery<T[Key]>;
|
|
370
|
+
} : string;
|
|
371
|
+
/**
|
|
372
|
+
* Utility type that ensures at least one property from the specified
|
|
373
|
+
* keys of a given type T is required, while the rest remain optional.
|
|
374
|
+
*
|
|
375
|
+
* This is useful for scenarios where you want to enforce that at least one
|
|
376
|
+
* of several optional (nullable) properties must be provided in an object.
|
|
377
|
+
*
|
|
378
|
+
* ```typescript
|
|
379
|
+
* type Example = AtLeastOne<{ a?: string; b?: number; c?: boolean }>;
|
|
380
|
+
* // Valid: { a: "hello" }, { b: 42 }, { c: true }, { a: "hello", b: 42 }
|
|
381
|
+
* // Invalid: {}, { a: undefined, b: undefined, c: undefined }
|
|
382
|
+
* ```
|
|
383
|
+
*/
|
|
384
|
+
type AtLeastOne<T, Keys extends keyof T = keyof T> = Keys extends keyof T ? Simplify<Required<Pick<T, Keys>> & Partial<Omit<T, Keys>>> : never;
|
|
320
385
|
|
|
321
386
|
/**
|
|
322
387
|
* Wraps a partial Zod object schema with:
|
|
@@ -356,4 +421,4 @@ declare const asQuery: <T extends z.ZodTypeAny>(schema: T) => z.ZodPreprocess<T>
|
|
|
356
421
|
*/
|
|
357
422
|
declare const isPlainObject: (value: unknown) => value is Record<string, unknown>;
|
|
358
423
|
|
|
359
|
-
export { type APIContractData, type APIContractError, type APIContractResult, type APIError, type APISuccess, type AsQuery, type AtLeastOne, EXCEPTION_STATUS_CODES, type ExceptionStatusCode, type FetchResult, type JWTServiceOptions, type JWTSignOptions, type MeasuredExecution, type Payload, type PayloadSchema, type ReplaceDotsWithUnderscores, SUCCESS_STATUS_CODES, type Simplify, type StringEnumRecord, type SuccessStatusCode, ZodJWTService, type ZodPaginationOptions, asQuery, createStringEnumRecord, failure, fetchAndThrow, fetchSafely, formatTime, generateRandomString, getColoredHTTPStatus, getFormattedDate, getFormattedTime, getUTCOffset, getZonedTime, honoLoggingHandler, isPlainObject, log, measureExecutionTime, onHandlerError, parseQueryValue, respond, safeExecute, sqlWhere, success, zodAtLeastOne, zodPaginationSchema, zodPaginationShape };
|
|
424
|
+
export { type APIContractData, type APIContractError, type APIContractResult, type APIError, type APISuccess, type AsQuery, type AtLeastOne, EXCEPTION_STATUS_CODES, type ExceptionStatusCode, type FetchResult, type JWTServiceOptions, type JWTSignOptions, type MeasuredExecution, type Payload, type PayloadSchema, type ReplaceDotsWithUnderscores, SUCCESS_STATUS_CODES, type Simplify, type StringEnumRecord, type SuccessStatusCode, ZodJWTService, type ZodPaginationOptions, type ZodSearchSchemaOptions, asQuery, createStringEnumRecord, failure, fetchAndThrow, fetchSafely, formatTime, generateRandomString, getColoredHTTPStatus, getFormattedDate, getFormattedTime, getUTCOffset, getZonedTime, honoLoggingHandler, isPlainObject, log, measureExecutionTime, onHandlerError, parseQueryValue, respond, safeExecute, sqlWhere, success, zodAtLeastOne, zodPaginationSchema, zodPaginationShape, zodSearchSchema };
|
package/dist/index.js
CHANGED
|
@@ -213,16 +213,25 @@ var ZodJWTService = class {
|
|
|
213
213
|
}
|
|
214
214
|
};
|
|
215
215
|
|
|
216
|
-
// src/zod-
|
|
216
|
+
// src/zod-search/zod-search.pagination.schemas.ts
|
|
217
217
|
import z from "zod";
|
|
218
218
|
var zodPaginationSchema = z.object({
|
|
219
|
-
/**
|
|
219
|
+
/**
|
|
220
|
+
* Zero-based number of records skipped before collecting a result page.
|
|
221
|
+
* String values are coerced to support validation of URL query input.
|
|
222
|
+
*/
|
|
220
223
|
offset: z.coerce.number().int().nonnegative().default(0),
|
|
221
|
-
/**
|
|
224
|
+
/**
|
|
225
|
+
* Positive maximum number of records returned in a single result page.
|
|
226
|
+
* String values are coerced to support validation of URL query input.
|
|
227
|
+
*/
|
|
222
228
|
limit: z.coerce.number().int().positive().default(10)
|
|
223
229
|
});
|
|
224
230
|
var zodPaginationShape = zodPaginationSchema.shape;
|
|
225
231
|
|
|
232
|
+
// src/zod-search/zod-search.schemas.ts
|
|
233
|
+
import z2 from "zod";
|
|
234
|
+
|
|
226
235
|
// src/zod-validation/zod-validation.refiners.ts
|
|
227
236
|
var DEFAULT_ERROR_MESSAGE = "Invalid input. At least one field must be provided.";
|
|
228
237
|
var zodAtLeastOne = (schema) => schema.superRefine((val, ctx) => {
|
|
@@ -231,8 +240,20 @@ var zodAtLeastOne = (schema) => schema.superRefine((val, ctx) => {
|
|
|
231
240
|
ctx.addIssue({ code: "custom", message: DEFAULT_ERROR_MESSAGE });
|
|
232
241
|
}).transform((val) => val);
|
|
233
242
|
|
|
243
|
+
// src/zod-search/zod-search.schemas.ts
|
|
244
|
+
var zodSearchQuerySchema = z2.string().trim().min(1).optional();
|
|
245
|
+
var createZodSearchWhereSchema = (filters) => zodAtLeastOne(filters.partial()).optional();
|
|
246
|
+
var zodSearchSchema = ({ filters, queryEnabled, paginationEnabled }) => {
|
|
247
|
+
const shape = {
|
|
248
|
+
where: createZodSearchWhereSchema(filters),
|
|
249
|
+
...queryEnabled !== false ? { query: zodSearchQuerySchema } : {},
|
|
250
|
+
...paginationEnabled !== false ? { pagination: zodPaginationSchema } : {}
|
|
251
|
+
};
|
|
252
|
+
return z2.object(shape).strict();
|
|
253
|
+
};
|
|
254
|
+
|
|
234
255
|
// src/zod-validation/zod-validation.parsing.ts
|
|
235
|
-
import
|
|
256
|
+
import z3 from "zod";
|
|
236
257
|
|
|
237
258
|
// src/zod-validation/zod-validation.utilities.ts
|
|
238
259
|
var isPlainObject = (value) => {
|
|
@@ -259,7 +280,7 @@ var parseQueryValue = (value) => {
|
|
|
259
280
|
return Number(normalized);
|
|
260
281
|
return value;
|
|
261
282
|
};
|
|
262
|
-
var asQuery = (schema) =>
|
|
283
|
+
var asQuery = (schema) => z3.preprocess(parseQueryValue, schema);
|
|
263
284
|
export {
|
|
264
285
|
EXCEPTION_STATUS_CODES,
|
|
265
286
|
SUCCESS_STATUS_CODES,
|
|
@@ -288,5 +309,6 @@ export {
|
|
|
288
309
|
success,
|
|
289
310
|
zodAtLeastOne,
|
|
290
311
|
zodPaginationSchema,
|
|
291
|
-
zodPaginationShape
|
|
312
|
+
zodPaginationShape,
|
|
313
|
+
zodSearchSchema
|
|
292
314
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kalutskii/foundation",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.23",
|
|
4
4
|
"description": "Typescript collection of most common utilities, schemas and functions among private projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"build": "tsup",
|
|
15
15
|
"lint": "eslint . --ext .ts --fix",
|
|
16
16
|
"format": "prettier --write .",
|
|
17
|
+
"test": "bun test",
|
|
17
18
|
"typecheck": "tsc --noEmit"
|
|
18
19
|
},
|
|
19
20
|
"exports": {
|