@kalutskii/foundation 0.6.8 → 0.6.10

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
@@ -12,7 +12,7 @@ Designed for use in Bun/Node.js and Cloudflare Workers environments.
12
12
  - Logging utility: unified colored log interface.
13
13
  - Random string generation utility.
14
14
  - JWT service with optional Zod schema validation.
15
- - Zod helpers for query param coercion, pagination schemas, and type utilities.
15
+ - Zod helpers for query param coercion, pagination schemas, nested schema flattening, and type utilities.
16
16
 
17
17
  ## Installation
18
18
 
@@ -144,7 +144,34 @@ 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
- ### 7. Use pagination schema
147
+ ### 8. Build flat query schemas from nested objects
148
+
149
+ ```ts
150
+ import { asQuerySchema } from '@kalutskii/foundation';
151
+ import { z } from 'zod';
152
+
153
+ const schema = asQuerySchema(
154
+ z.object({
155
+ search: z.string().optional(),
156
+ sort: z
157
+ .object({
158
+ field: z.string(),
159
+ order: z.enum(['asc', 'desc']),
160
+ })
161
+ .optional(),
162
+ })
163
+ );
164
+
165
+ schema.parse({ search: 'foo', field: 'name', order: 'asc' });
166
+ // { search: 'foo', field: 'name', order: 'asc' }
167
+
168
+ // Rejects the original nested structure
169
+ schema.parse({ sort: { field: 'name', order: 'asc' } }); // throws
170
+ ```
171
+
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
+
174
+ ### 8. Use pagination schema
148
175
 
149
176
  ```ts
150
177
  import { refinePagination, zodPaginationSchema, zodPaginationShape } from '@kalutskii/foundation';
@@ -166,7 +193,7 @@ await db.query.users.findMany({
166
193
  });
167
194
  ```
168
195
 
169
- ### 8. Work with JWT using `ZodJWTService`
196
+ ### 9. Work with JWT using `ZodJWTService`
170
197
 
171
198
  ```ts
172
199
  import { ZodJWTService } from '@kalutskii/foundation';
@@ -183,7 +210,7 @@ const payload = await jwt.verifyOrThrow(token, 'secret');
183
210
  const decoded = await jwt.decode(token);
184
211
  ```
185
212
 
186
- ### 9. Datetime helpers
213
+ ### 10. Datetime helpers
187
214
 
188
215
  ```ts
189
216
  import { formatTime, getFormattedDate, getFormattedTime, getZonedTime } from '@kalutskii/foundation';
@@ -194,7 +221,7 @@ getFormattedDate({ tz: 'Europe/Moscow', withTime: false }); // '22.06.2026'
194
221
  formatTime(new Date(), { tz: 'Europe/Moscow' }); // '15:30:00, 22 июня 2026 (+3 UTC)'
195
222
  ```
196
223
 
197
- ### 10. Logging
224
+ ### 11. Logging
198
225
 
199
226
  ```ts
200
227
  import { log } from '@kalutskii/foundation';
@@ -214,6 +241,7 @@ The package exports all public APIs from a single entrypoint:
214
241
  - **Utilities**: `safeExecute`, `measureExecutionTime`, `generateRandomString`, `log`, `getColoredHTTPStatus`, datetime helpers.
215
242
  - **Zod JWT**: `ZodJWTService`.
216
243
  - **Zod Pagination**: `zodPaginationSchema`, `zodPaginationShape`, `refinePagination`, `ZodPaginationOptions`.
244
+ - **Zod Flatten**: `asQuerySchema`, `flattenZodShape`, `isZodObject`, `isZodOptional`, `unwrapOptional`, `FlattenZodShape`.
217
245
  - **Zod Validation**: `asQuery`, `AsQuery`, `AtLeastOne`.
218
246
  - **Type Utilities**: `Simplify`.
219
247
 
package/dist/index.d.ts CHANGED
@@ -58,36 +58,14 @@ type FetchResult<T> = {
58
58
  data: null;
59
59
  };
60
60
 
61
- /**
62
- * Utility type that simplifies a given type T by flattening its structure.
63
- * This is particularly useful for improving the readability of complex types.
64
- */
65
- type Simplify<T> = {
66
- [K in keyof T]: T[K];
67
- } & {};
68
-
69
61
  type QueryPrimitive = string | number | boolean | bigint | Date | null | undefined;
70
62
  /**
71
- * Type utility that recursively transforms all fields to string, as well as handling arrays and objects.
72
- * This is useful for serializing complex data structures into query parameters, which must be strings.
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).
73
65
  */
74
66
  type AsQuery<T> = T extends QueryPrimitive ? string : T extends readonly (infer Item)[] ? AsQuery<Item>[] : T extends object ? {
75
67
  [Key in keyof T]: AsQuery<T[Key]>;
76
68
  } : string;
77
- /**
78
- * Utility type that ensures at least one property from the specified
79
- * keys of a given type T is required, while the rest remain optional.
80
- *
81
- * This is useful for scenarios where you want to enforce that at least one
82
- * of several optional (nullable) properties must be provided in an object.
83
- *
84
- * ```ts
85
- * type Example = AtLeastOne<{ a?: string; b?: number; c?: boolean }>;
86
- * // Valid: { a: "hello" }, { b: 42 }, { c: true }, { a: "hello", b: 42 }
87
- * // Invalid: {}, { a: undefined, b: undefined, c: undefined }
88
- * ```
89
- */
90
- type AtLeastOne<T, Keys extends keyof T = keyof T> = Keys extends keyof T ? Simplify<Required<Pick<T, Keys>> & Partial<Omit<T, Keys>>> : never;
91
69
 
92
70
  /**
93
71
  * Wraps c.json with a typed success payload / (or void data) & possible APIError response.
@@ -227,6 +205,28 @@ declare const log: {
227
205
  error: (message: string, service?: string, stack?: string) => void;
228
206
  };
229
207
 
208
+ /**
209
+ * Utility type that simplifies a given type T by flattening its structure.
210
+ * This is particularly useful for improving the readability of complex types.
211
+ */
212
+ type Simplify<T> = {
213
+ [K in keyof T]: T[K];
214
+ } & {};
215
+ /**
216
+ * Utility type that ensures at least one property from the specified
217
+ * keys of a given type T is required, while the rest remain optional.
218
+ *
219
+ * This is useful for scenarios where you want to enforce that at least one
220
+ * of several optional (nullable) properties must be provided in an object.
221
+ *
222
+ * ```ts
223
+ * type Example = AtLeastOne<{ a?: string; b?: number; c?: boolean }>;
224
+ * // Valid: { a: "hello" }, { b: 42 }, { c: true }, { a: "hello", b: 42 }
225
+ * // Invalid: {}, { a: undefined, b: undefined, c: undefined }
226
+ * ```
227
+ */
228
+ type AtLeastOne<T, Keys extends keyof T = keyof T> = Keys extends keyof T ? Simplify<Required<Pick<T, Keys>> & Partial<Omit<T, Keys>>> : never;
229
+
230
230
  /** Options for configuring the JWT service. */
231
231
  type JWTServiceOptions = {
232
232
  /** The algorithm to use for signing and verifying JWTs. Defaults to 'HS256'. */
@@ -315,6 +315,77 @@ type ZodPaginationOptions = z.infer<typeof zodPaginationSchema>;
315
315
  */
316
316
  declare const refinePagination: (options?: ZodPaginationOptions) => ZodPaginationOptions;
317
317
 
318
+ type ZodShape = Record<string, z.ZodTypeAny>;
319
+ type AnyZodObject = z.ZodObject<ZodShape>;
320
+ type UnionToIntersection<T> = (T extends unknown ? (value: T) => void : never) extends (value: infer I) => void ? I : never;
321
+ type UnwrapOptional<T extends z.ZodTypeAny> = T extends z.ZodOptional<infer Inner> ? Inner : T;
322
+ type IsOptional<T extends z.ZodTypeAny> = T extends z.ZodOptional<z.ZodTypeAny> ? true : false;
323
+ type Optionalize<TSchema extends z.ZodTypeAny, TShouldOptionalize extends boolean> = TShouldOptionalize extends true ? z.ZodOptional<TSchema> : TSchema;
324
+ type FlattenObjectField<TSchema extends z.ZodTypeAny> = UnwrapOptional<TSchema> extends z.ZodObject<infer InnerShape extends ZodShape> ? {
325
+ [K in keyof InnerShape]: Optionalize<InnerShape[K], IsOptional<TSchema>>;
326
+ } : never;
327
+ type FlattenNestedObjectFields<TShape extends ZodShape> = {
328
+ [K in keyof TShape]: FlattenObjectField<TShape[K]>;
329
+ }[keyof TShape];
330
+ /**
331
+ * The resulting shape after all nested `z.ZodObject` fields have been lifted to the top level.
332
+ * Primitive fields are kept as-is; nested object fields are replaced with their inner keys.
333
+ * If the parent field was optional, all promoted inner fields become optional as well.
334
+ */
335
+ type FlattenZodShape<TShape extends ZodShape> = {
336
+ [K in keyof TShape as UnwrapOptional<TShape[K]> extends AnyZodObject ? never : K]: TShape[K];
337
+ } & UnionToIntersection<FlattenNestedObjectFields<TShape>>;
338
+
339
+ /**
340
+ * Recursively lifts all nested `z.ZodObject` fields to the top level of the shape.
341
+ * Primitive fields are preserved as-is. If the parent field was optional, all promoted
342
+ * inner keys become optional to avoid requiring fields that were previously nested under an optional object.
343
+ *
344
+ * ```ts
345
+ * const flat = flattenZodShape({
346
+ * name: z.string(),
347
+ * address: z.object({ city: z.string(), zip: z.string() }).optional(),
348
+ * });
349
+ * // { name: z.string(), city: z.string().optional(), zip: z.string().optional() }
350
+ * ```
351
+ */
352
+ declare const flattenZodShape: <TShape extends ZodShape>(shape: TShape) => FlattenZodShape<TShape>;
353
+ /**
354
+ * Builds a strict flat query schema from a nested `z.ZodObject`.
355
+ * All nested object fields are lifted to the top level while preserving optionality,
356
+ * making the result suitable for validating flat query parameter objects.
357
+ *
358
+ * ```ts
359
+ * const schema = asQuerySchema(
360
+ * z.object({
361
+ * search: z.string().optional(),
362
+ * sort: z.object({ field: z.string(), order: z.enum(['asc', 'desc']) }).optional(),
363
+ * })
364
+ * );
365
+ *
366
+ * schema.parse({ search: 'foo', field: 'name', order: 'asc' });
367
+ * // { search: 'foo', field: 'name', order: 'asc' }
368
+ * ```
369
+ */
370
+ declare const asQuerySchema: <TShape extends ZodShape>(schema: z.ZodObject<TShape>) => z.ZodObject<FlattenZodShape<TShape> extends infer T ? { -readonly [P in keyof T]: T[P]; } : never, z.core.$strict>;
371
+
372
+ /**
373
+ * Checks if the provided schema is an instance of `z.ZodObject`.
374
+ * @returns `true` if the schema is a `z.ZodObject`, `false` otherwise.
375
+ */
376
+ declare const isZodObject: (schema: z.ZodTypeAny) => schema is AnyZodObject;
377
+ /**
378
+ * Checks if the provided schema is an instance of `z.ZodOptional`.
379
+ * @returns `true` if the schema is a `z.ZodOptional`, `false` otherwise.
380
+ */
381
+ declare const isZodOptional: (schema: z.ZodTypeAny) => schema is z.ZodOptional<z.ZodTypeAny>;
382
+ /**
383
+ * Unwraps a `z.ZodOptional` schema to retrieve its inner schema.
384
+ * If the provided schema is not optional, it is returned unchanged.
385
+ */
386
+ declare const unwrapOptional: (schema: z.ZodTypeAny) => z.ZodTypeAny;
387
+
388
+ declare const parseQueryValue: (value: unknown) => unknown;
318
389
  /**
319
390
  * Query parameters always arrive as strings, even when they semantically represent numbers
320
391
  * or booleans, so we preprocess them to convert to the appropriate types before validation.
@@ -334,4 +405,10 @@ declare const refinePagination: (options?: ZodPaginationOptions) => ZodPaginatio
334
405
  */
335
406
  declare const asQuery: <T extends z.ZodTypeAny>(schema: T) => z.ZodPreprocess<T>;
336
407
 
337
- 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, SUCCESS_STATUS_CODES, type Simplify, type SuccessStatusCode, ZodJWTService, type ZodPaginationOptions, asQuery, failure, fetchAndThrow, fetchSafely, formatTime, generateRandomString, getColoredHTTPStatus, getFormattedDate, getFormattedTime, getUTCOffset, getZonedTime, honoLoggingHandler, log, measureExecutionTime, onHandlerError, refinePagination, respond, safeExecute, sqlWhere, success, zodPaginationSchema, zodPaginationShape };
408
+ /**
409
+ * Checks if the given value is a plain object (i.e., an object
410
+ * that is not null, not an array, and has a prototype of Object).
411
+ */
412
+ declare const isPlainObject: (value: unknown) => value is Record<string, unknown>;
413
+
414
+ export { type APIContractData, type APIContractError, type APIContractResult, type APIError, type APISuccess, type AnyZodObject, type AsQuery, type AtLeastOne, EXCEPTION_STATUS_CODES, type ExceptionStatusCode, type FetchResult, type FlattenZodShape, type JWTServiceOptions, type JWTSignOptions, type MeasuredExecution, type Payload, type PayloadSchema, SUCCESS_STATUS_CODES, type Simplify, type SuccessStatusCode, ZodJWTService, type ZodPaginationOptions, type ZodShape, asQuery, asQuerySchema, failure, fetchAndThrow, fetchSafely, flattenZodShape, formatTime, generateRandomString, getColoredHTTPStatus, getFormattedDate, getFormattedTime, getUTCOffset, getZonedTime, honoLoggingHandler, isPlainObject, isZodObject, isZodOptional, log, measureExecutionTime, onHandlerError, parseQueryValue, refinePagination, respond, safeExecute, sqlWhere, success, unwrapOptional, zodPaginationSchema, zodPaginationShape };
package/dist/index.js CHANGED
@@ -245,14 +245,55 @@ var zodPaginationSchema = z2.object({
245
245
  limit: asQuery(z2.number().int().positive()).optional()
246
246
  });
247
247
  var zodPaginationShape = zodPaginationSchema.shape;
248
+
249
+ // src/zod-flatten/zod-flatten.schemas.ts
250
+ import z4 from "zod";
251
+
252
+ // src/zod-flatten/zod-flatten.utilities.ts
253
+ import z3 from "zod";
254
+ var isZodObject = (schema) => {
255
+ return schema instanceof z3.ZodObject;
256
+ };
257
+ var isZodOptional = (schema) => {
258
+ return schema instanceof z3.ZodOptional;
259
+ };
260
+ var unwrapOptional = (schema) => {
261
+ return isZodOptional(schema) ? schema.unwrap() : schema;
262
+ };
263
+
264
+ // src/zod-flatten/zod-flatten.schemas.ts
265
+ var flattenZodShape = (shape) => {
266
+ const flat = {};
267
+ for (const [key, schema] of Object.entries(shape)) {
268
+ const unwrappedSchema = unwrapOptional(schema);
269
+ if (isZodObject(unwrappedSchema) === false) {
270
+ flat[key] = schema;
271
+ continue;
272
+ }
273
+ for (const [innerKey, innerSchema] of Object.entries(unwrappedSchema.shape)) {
274
+ flat[innerKey] = isZodOptional(schema) ? innerSchema.optional() : innerSchema;
275
+ }
276
+ }
277
+ return flat;
278
+ };
279
+ var asQuerySchema = (schema) => {
280
+ return z4.object(flattenZodShape(schema.shape)).strict();
281
+ };
282
+
283
+ // src/zod-validation/zod-validation.utilities.ts
284
+ var isPlainObject = (value) => {
285
+ return typeof value === "object" && value !== null && !Array.isArray(value);
286
+ };
248
287
  export {
249
288
  EXCEPTION_STATUS_CODES,
250
289
  SUCCESS_STATUS_CODES,
251
290
  ZodJWTService,
252
291
  asQuery,
292
+ asQuerySchema,
253
293
  failure,
254
294
  fetchAndThrow,
255
295
  fetchSafely,
296
+ flattenZodShape,
256
297
  formatTime,
257
298
  generateRandomString,
258
299
  getColoredHTTPStatus,
@@ -261,14 +302,19 @@ export {
261
302
  getUTCOffset,
262
303
  getZonedTime,
263
304
  honoLoggingHandler,
305
+ isPlainObject,
306
+ isZodObject,
307
+ isZodOptional,
264
308
  log,
265
309
  measureExecutionTime,
266
310
  onHandlerError,
311
+ parseQueryValue,
267
312
  refinePagination,
268
313
  respond,
269
314
  safeExecute,
270
315
  sqlWhere,
271
316
  success,
317
+ unwrapOptional,
272
318
  zodPaginationSchema,
273
319
  zodPaginationShape
274
320
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kalutskii/foundation",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
4
4
  "description": "Typescript collection of most common utilities, schemas and functions among private projects.",
5
5
  "type": "module",
6
6
  "repository": {