@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 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
- ### 8. Build flat query schemas from nested objects
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. Use pagination schema
174
+ ### 8. Build reusable search schemas
175
175
 
176
176
  ```typescript
177
- import { refinePagination, zodPaginationSchema, zodPaginationShape } from '@kalutskii/foundation';
177
+ import { zodPaginationSchema, zodSearchSchema } from '@kalutskii/foundation';
178
178
  import { z } from 'zod';
179
179
 
180
- // Use as a standalone schema
181
- const options = zodPaginationSchema.parse({ offset: '0', limit: '20' });
182
- // { offset: 0, limit: 20 }
180
+ const assetSchema = z.object({
181
+ status: z.enum(['active', 'archived']),
182
+ categoryId: z.number(),
183
+ });
183
184
 
184
- // Spread shape into a larger query schema
185
- const querySchema = z.object({
186
- search: z.string().optional(),
187
- ...zodPaginationShape,
185
+ const assetSearchSchema = zodSearchSchema({
186
+ filters: assetSchema.pick({ status: true, categoryId: true }),
188
187
  });
189
188
 
190
- // Strip undefined pagination values before passing to a query builder (e.g. Drizzle)
191
- await db.query.users.findMany({
192
- ...refinePagination(options),
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 Pagination**: `zodPaginationSchema`, `zodPaginationShape`, `refinePagination`, `ZodPaginationOptions`.
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, Simplify as Simplify$1 } from 'drizzle-orm';
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<AsQuery<T>> | APIError, S, 'json'>;
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
- * Reusable pagination fields for flat query schemas, allowing for
306
- * consistent validation of pagination parameters across different endpoints.
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 type representing the pagination options validated by the `zodPaginationSchema`.
317
- * Read documentation for `zodPaginationSchema` for more details on the structure and usage of this type.
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-pagination/zod-pagination.schemas.ts
216
+ // src/zod-search/zod-search.pagination.schemas.ts
217
217
  import z from "zod";
218
218
  var zodPaginationSchema = z.object({
219
- /** Integer representing the starting point for pagination. */
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
- /** Integer representing the maximum number of items to return. */
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 z2 from "zod";
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) => z2.preprocess(parseQueryValue, 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.21",
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": {